pour-engine 1.42.4 → 1.43.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/config/project.config.js +1 -1
- package/engine/COVERAGE.md +2 -2
- package/engine/README.md +1 -1
- package/engine/lib/contrast.js +8 -0
- package/engine/rules/index.js +7 -6
- package/engine/rules/wcag/1.4.3-color-contrast.js +1 -7
- package/engine/rules/wcag/1.4.3-control-contrast.js +8 -5
- package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +56 -40
- package/engine/rules/wcag/4.1.2-aria-state-unreachable.js +56 -0
- package/package.json +1 -1
package/config/project.config.js
CHANGED
package/engine/COVERAGE.md
CHANGED
|
@@ -113,7 +113,7 @@ meaning that only a person can settle, even where a rule decides most real pages
|
|
|
113
113
|
|
|
114
114
|
| SC | Name | Level | Coverage | Rules |
|
|
115
115
|
|---|---|---|---|---|
|
|
116
|
-
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-label-misuse, aria-valid-refs, button-name, composite-widget-name, frame-title, invoker-target, label-for-valid, nested-interactive, role-required-aria, summary-name, valid-role |
|
|
116
|
+
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-label-misuse, aria-state-unreachable, aria-valid-refs, button-name, composite-widget-name, frame-title, invoker-target, label-for-valid, nested-interactive, role-required-aria, summary-name, valid-role |
|
|
117
117
|
| 4.1.3 | [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) | AA | Manual review | — |
|
|
118
118
|
|
|
119
|
-
**Totals:** 51 partially automated · 35 manual — with
|
|
119
|
+
**Totals:** 51 partially automated · 35 manual — with 99 rules.
|
package/engine/README.md
CHANGED
|
@@ -117,7 +117,7 @@ assessment, including interaction states and any checks reported as skipped.
|
|
|
117
117
|
|
|
118
118
|
## Honest current state
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
99 rules, hardened on real sites and the `testbeds/` pages. Audits walk into
|
|
121
121
|
open shadow roots (targets come back as `host >>> inner` selector chains);
|
|
122
122
|
iframe traversal is the host's job, and the extension does it. Known gaps:
|
|
123
123
|
the full accname spec (ours is a close approximation) and the language
|
package/engine/lib/contrast.js
CHANGED
|
@@ -101,6 +101,14 @@ export function contrastRatio(foreground, background) {
|
|
|
101
101
|
* (no text threshold lives up there), and "21:1" reads better than
|
|
102
102
|
* "21.0:1" — a trailing zero implies precision the eye can use.
|
|
103
103
|
*/
|
|
104
|
+
/** Serialise a judged colour for the report and the checker link. Composited
|
|
105
|
+
* channels keep up to two decimals: rounding them to integers changes the
|
|
106
|
+
* pair enough that the checker scores a different ratio than the audit
|
|
107
|
+
* reported (4.21 vs 4.19 on a 70%-opacity footer). Shared by the text and
|
|
108
|
+
* the form-control contrast rules (2026-09-17). */
|
|
109
|
+
const channelText = (value) => String(Math.round(value * 100) / 100);
|
|
110
|
+
export const asRgb = (color) => `rgb(${channelText(color.r)}, ${channelText(color.g)}, ${channelText(color.b)})`;
|
|
111
|
+
|
|
104
112
|
export function showRatio(ratio) {
|
|
105
113
|
const text = ratio >= 10
|
|
106
114
|
? (Math.floor(ratio * 10) / 10).toFixed(1)
|
package/engine/rules/index.js
CHANGED
|
@@ -42,13 +42,13 @@ import audioControl from './wcag/1.4.2-audio-control.js';
|
|
|
42
42
|
import pauseStopHide from './wcag/2.2.2-pause-stop-hide.js';
|
|
43
43
|
import mediaCaptions from './wcag/1.2.2-media-captions.js';
|
|
44
44
|
import ariaAttrValid from './wcag/4.1.2-aria-attr-valid.js';
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// instead of claiming a failure (see the note in the rule file).
|
|
45
|
+
// An unsupported ARIA attribute breaks an ARIA author MUST (1.2 §8.6) but is
|
|
46
|
+
// INERT, so name, role and value survive: asserted at minor since 2026-09-17
|
|
47
|
+
// (it reviewed before). Where the DOM proves the element really has the
|
|
48
|
+
// state (a native readonly, required, disabled or checked beside the aria-
|
|
49
|
+
// attribute) the finding is aria-state-unreachable's, at moderate.
|
|
51
50
|
import ariaAllowedAttr from './wcag/4.1.2-aria-allowed-attr.js';
|
|
51
|
+
import ariaStateUnreachable from './wcag/4.1.2-aria-state-unreachable.js';
|
|
52
52
|
import ariaFieldName from './wcag/4.1.2-aria-field-name.js';
|
|
53
53
|
// Asks instead of asserting: a prohibited name on a generic element is a
|
|
54
54
|
// real ARIA MUST violation the browser silently drops, so it runs in the
|
|
@@ -191,6 +191,7 @@ export default [
|
|
|
191
191
|
mediaCaptions,
|
|
192
192
|
ariaAttrValid,
|
|
193
193
|
ariaAllowedAttr,
|
|
194
|
+
ariaStateUnreachable,
|
|
194
195
|
ariaFieldName,
|
|
195
196
|
ariaLabelMisuse,
|
|
196
197
|
roleRequiredAria,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
opacityAnimating, restingOpacity, mediaRects, inZeroClipSubtree,
|
|
7
7
|
paintedBackdrop, opaquePanelRects, viewportVeil, textShadowHalo, textShadowNegligible,
|
|
8
8
|
pseudoBackdropForText, filmedContrastBounds, backgroundColorSource, scrimPaint, applyOverlays,
|
|
9
|
-
showRatio, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop } from '../../lib/contrast.js';
|
|
9
|
+
showRatio, asRgb, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop } from '../../lib/contrast.js';
|
|
10
10
|
|
|
11
11
|
/** The first url() among a background-image list's layers, or null. */
|
|
12
12
|
const firstLayerUrl = (css) => splitBackgroundLayers(css ?? '').map(backgroundLayerUrl).find(Boolean) ?? null;
|
|
@@ -20,12 +20,6 @@ const extentOf = (rect) => (rect ? { width: rect.right - rect.left, height: rect
|
|
|
20
20
|
* every character that carries grouping, rating or code meaning. */
|
|
21
21
|
const SEPARATOR_GLYPHS = /^([|¦·•∙‧/⁄\\‐‑‒–—―⁃«»‹›-])\1*$/;
|
|
22
22
|
|
|
23
|
-
/** Serialise a judged colour for the report and the checker link. Composited
|
|
24
|
-
* channels keep up to two decimals: rounding them to integers changes the
|
|
25
|
-
* pair enough that the checker scores a different ratio than the audit
|
|
26
|
-
* reported (4.21 vs 4.19 on a 70%-opacity footer). */
|
|
27
|
-
const channelText = (value) => String(Math.round(value * 100) / 100);
|
|
28
|
-
const asRgb = (color) => `rgb(${channelText(color.r)}, ${channelText(color.g)}, ${channelText(color.b)})`;
|
|
29
23
|
|
|
30
24
|
/** The one case a full-page veil still can't be judged for the author: the
|
|
31
25
|
* resting page and the dimmed one land on opposite sides of the threshold,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import {
|
|
6
6
|
parseColor, contrastRatio, composite, effectiveBackground, isLargeText,
|
|
7
7
|
backgroundImageSource, cumulativeOpacity, opacityAnimating, restingOpacity, showRatio,
|
|
8
|
-
hasPaintEffects, opacityGroupPaint, splitBackgroundLayers, sampledGradientRange,
|
|
8
|
+
hasPaintEffects, opacityGroupPaint, splitBackgroundLayers, sampledGradientRange, asRgb,
|
|
9
9
|
} from '../../lib/contrast.js';
|
|
10
10
|
|
|
11
11
|
export default {
|
|
@@ -108,7 +108,7 @@ export default {
|
|
|
108
108
|
if (group?.unresolved) { unresolvedGroup = true; return null; }
|
|
109
109
|
if (group) {
|
|
110
110
|
const ratio = contrastRatio(group.foreground, group.background);
|
|
111
|
-
return ratio >= required ? null : { what, ratio };
|
|
111
|
+
return ratio >= required ? null : { what, ratio, fg: group.foreground, bg: group.background };
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
const painted = opacity * ownOpacity;
|
|
@@ -116,7 +116,7 @@ export default {
|
|
|
116
116
|
const fg = faded.a < 1 ? composite(faded, background) : faded;
|
|
117
117
|
const ratio = contrastRatio(fg, background);
|
|
118
118
|
if (ratio >= required) return null;
|
|
119
|
-
return { what, ratio };
|
|
119
|
+
return { what, ratio, fg, bg: background };
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
const failures = [];
|
|
@@ -151,8 +151,11 @@ export default {
|
|
|
151
151
|
return {
|
|
152
152
|
status: 'fail',
|
|
153
153
|
message: `This field's ${worst.what} has ${showRatio(worst.ratio)}:1 contrast against the field background — below the ${required}:1 minimum.`,
|
|
154
|
-
fix: `Darken the ${worst.what.includes('placeholder') ? 'placeholder colour (::placeholder)' : 'text colour'} until it reaches ${required}:1 against the field background.`,
|
|
155
|
-
|
|
154
|
+
fix: `Darken the ${worst.what.includes('placeholder') ? 'placeholder colour (::placeholder)' : 'text colour'} until it reaches ${required}:1 against the field background (currently ${asRgb(worst.fg)} on ${asRgb(worst.bg)}).`,
|
|
155
|
+
// The judged pair, as the text rule reports it, so the UIs can link
|
|
156
|
+
// out to the checker with it (the link was missing on this rule's
|
|
157
|
+
// findings until 2026-09-17: it carried the ratio and nothing else).
|
|
158
|
+
data: { foreground: asRgb(worst.fg), background: asRgb(worst.bg), ratio: Number(showRatio(worst.ratio)), required },
|
|
156
159
|
};
|
|
157
160
|
},
|
|
158
161
|
};
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
// ARIA 1.2 §8.6, repeated verbatim in the 1.3 draft: "authors MUST only use
|
|
2
2
|
// non-global states and properties on elements with a role supporting the
|
|
3
3
|
// state or property". An unsupported attribute breaks that MUST, so this is a
|
|
4
|
-
// real, provable authoring error and the rule asserts it
|
|
4
|
+
// real, provable authoring error, and since 2026-09-17 the rule asserts it
|
|
5
|
+
// as a violation at minor impact (David: invalid according to the spec is
|
|
6
|
+
// marked as such, low). It reviewed until then, on the reasoning below,
|
|
7
|
+
// which still decides the impact.
|
|
5
8
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// while the same attribute on
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// An unsupported attribute is INERT under Core-AAM: user agents expose the
|
|
10
|
+
// states and properties a role supports, so aria-expanded on a button
|
|
11
|
+
// reaches assistive technology as expanded=true while the same attribute on
|
|
12
|
+
// complementary, heading or list reaches nothing (Chromium measured the
|
|
13
|
+
// same, as a starting point, not the arbiter). It is dropped, not
|
|
14
|
+
// misreported. The role is still exposed, the name is still computed, and
|
|
15
|
+
// nothing is announced incorrectly, which is why the impact is minor and
|
|
16
|
+
// not serious.
|
|
12
17
|
//
|
|
13
|
-
// What
|
|
14
|
-
// the state the author reached for, that state is now
|
|
15
|
-
// reader
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// does not actually cause; staying silent leaves a genuine authoring error
|
|
23
|
-
// unreported. Asking is the honest third option, and it is the one the
|
|
24
|
-
// engine is built around.
|
|
18
|
+
// What makes it more than a slip is the thing underneath: if the element
|
|
19
|
+
// genuinely has the state the author reached for, that state is now
|
|
20
|
+
// invisible to a screen reader, and that is a 4.1.2 failure in the full
|
|
21
|
+
// sense. The DOM sometimes proves it: a native readonly, required, disabled
|
|
22
|
+
// or checked attribute beside the aria- one (a text input given
|
|
23
|
+
// role="region" with readonly and aria-readonly on privatebanking.hsbc.com,
|
|
24
|
+
// 2026-09-17). Those attributes belong to aria-state-unreachable, which
|
|
25
|
+
// asserts them at moderate; this rule leaves them to it so an element gets
|
|
26
|
+
// one finding per attribute, and keeps the stray ones.
|
|
25
27
|
//
|
|
26
28
|
// Note "unsupported" is distinct from "prohibited" (§5.2.5), a separate
|
|
27
|
-
// category about naming that this rule does not cover
|
|
29
|
+
// category about naming that this rule does not cover; aria-label and
|
|
30
|
+
// aria-labelledby misuse have their own rule.
|
|
28
31
|
//
|
|
29
32
|
// Worth knowing when comparing against other tools: aria-expanded WAS
|
|
30
33
|
// inherited into complementary, banner, heading, img and list under ARIA 1.1,
|
|
@@ -38,38 +41,51 @@ import { GLOBAL_ARIA, ROLE_ARIA, KNOWN_ARIA, effectiveRole } from '../../lib/rol
|
|
|
38
41
|
// (aria-label-misuse) — excluded here to avoid double-reporting.
|
|
39
42
|
const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
|
|
40
43
|
|
|
44
|
+
// aria- states whose native HTML attribute of the same name proves the
|
|
45
|
+
// element really has the state the unsupported attribute was reaching for.
|
|
46
|
+
// Those are aria-state-unreachable's, which imports this.
|
|
47
|
+
export const NATIVE_STATE = { readonly: 'readonly', required: 'required', disabled: 'disabled', checked: 'checked' };
|
|
48
|
+
|
|
49
|
+
/** The element's role and the non-global aria- attributes (names without
|
|
50
|
+
* the prefix) that role does not support, or null when there are none or
|
|
51
|
+
* the role is not modelled. Shared with aria-state-unreachable. */
|
|
52
|
+
export function unsupportedAria(element) {
|
|
53
|
+
const ariaAttrs = [];
|
|
54
|
+
for (const { name } of attributesOf(element)) {
|
|
55
|
+
if (!name.startsWith('aria-')) continue;
|
|
56
|
+
const attr = name.slice(5);
|
|
57
|
+
// Unknown/misspelled attributes are aria-attr-valid's finding — judging
|
|
58
|
+
// them against a role too would report the same typo twice.
|
|
59
|
+
if (!HANDLED_ELSEWHERE.has(attr) && KNOWN_ARIA.has(attr)) ariaAttrs.push(attr);
|
|
60
|
+
}
|
|
61
|
+
if (!ariaAttrs.length) return null;
|
|
62
|
+
const role = effectiveRole(element);
|
|
63
|
+
const allowed = role && ROLE_ARIA[role];
|
|
64
|
+
if (!allowed) return null; // role not modelled: don't guess
|
|
65
|
+
const names = ariaAttrs.filter((name) => !GLOBAL_ARIA.has(name) && !allowed.includes(name));
|
|
66
|
+
return names.length ? { role, names } : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
export default {
|
|
42
70
|
id: 'aria-allowed-attr',
|
|
43
71
|
name: 'Allowed ARIA attributes',
|
|
44
|
-
impact: '
|
|
72
|
+
impact: 'minor',
|
|
45
73
|
tags: ['wcag2a', 'wcag412'],
|
|
46
74
|
help: 'ARIA attributes must be supported by the element’s role',
|
|
47
75
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
|
|
48
76
|
selector: '*',
|
|
49
77
|
visibleOnly: false,
|
|
50
78
|
evaluate(element) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!name.startsWith('aria-')) continue;
|
|
56
|
-
const attr = name.slice(5);
|
|
57
|
-
// Unknown/misspelled attributes are aria-attr-valid's finding — judging
|
|
58
|
-
// them against a role too would report the same typo twice.
|
|
59
|
-
if (!HANDLED_ELSEWHERE.has(attr) && KNOWN_ARIA.has(attr)) ariaAttrs.push(attr);
|
|
60
|
-
}
|
|
61
|
-
if (!ariaAttrs.length) return { status: 'pass' };
|
|
62
|
-
|
|
63
|
-
const role = effectiveRole(element);
|
|
64
|
-
const allowed = role && ROLE_ARIA[role];
|
|
65
|
-
if (!allowed) return { status: 'pass' }; // role not modelled: don't guess
|
|
66
|
-
|
|
67
|
-
const disallowed = ariaAttrs.filter((name) => !GLOBAL_ARIA.has(name) && !allowed.includes(name));
|
|
79
|
+
const found = unsupportedAria(element);
|
|
80
|
+
if (!found) return { status: 'pass' };
|
|
81
|
+
// A state the DOM proves the element has is aria-state-unreachable's.
|
|
82
|
+
const disallowed = found.names.filter((name) => !(NATIVE_STATE[name] && element.hasAttribute(NATIVE_STATE[name])));
|
|
68
83
|
if (!disallowed.length) return { status: 'pass' };
|
|
84
|
+
const role = found.role;
|
|
69
85
|
const names = disallowed.map((n) => `aria-${n}`);
|
|
70
86
|
return {
|
|
71
|
-
status: '
|
|
72
|
-
message: `${names.join(', ')} is not supported on role "${role}", so
|
|
87
|
+
status: 'fail',
|
|
88
|
+
message: `${names.join(', ')} is not supported on role "${role}" (ARIA 1.2 §8.6), so user agents drop it and assistive technology never sees it. Nothing is announced wrongly, but nothing is announced at all. If the element really has that state, it is invisible to a screen reader; if it does not, the attribute is stray and no one is affected, but ARIA 1.2 still says authors MUST NOT write it.`,
|
|
73
89
|
fix: `Move ${names.join('/')} to the element whose role supports it, usually the control that toggles this one, or remove it if the element has no such state. Adding a role to this element to make the attribute legal is rarely right: the host language restricts which roles each element may take.`,
|
|
74
90
|
};
|
|
75
91
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// WCAG SC 4.1.2 Name, Role, Value (Level A)
|
|
2
|
+
// The proven half of aria-allowed-attr, split out on 2026-09-17 so it can
|
|
3
|
+
// carry its own impact. That rule asserts every non-global ARIA attribute
|
|
4
|
+
// an element's role does not support (ARIA 1.2 §8.6, an author MUST) at
|
|
5
|
+
// minor, because such an attribute is inert: user agents expose only the
|
|
6
|
+
// states a role supports, so nothing false is announced and the name and
|
|
7
|
+
// role survive. Most of the time the attribute is stray and nobody is
|
|
8
|
+
// affected.
|
|
9
|
+
//
|
|
10
|
+
// This rule fires when the DOM proves the state is real: a native readonly,
|
|
11
|
+
// required, disabled or checked attribute sits beside the unsupported aria-
|
|
12
|
+
// one. Then the element genuinely has the state, the host language applies
|
|
13
|
+
// it, and no assistive technology can learn it through the role the author
|
|
14
|
+
// chose. That is what 4.1.2 asks for and does not get, so the impact is
|
|
15
|
+
// moderate: a screen reader user meets a readonly field announced as a
|
|
16
|
+
// region and tries to type into it, or a required field they cannot tell is
|
|
17
|
+
// required. Moderate rather than serious because the control still works
|
|
18
|
+
// and its value is still there; the state is what is lost.
|
|
19
|
+
//
|
|
20
|
+
// The shape it was written for: four calculator output fields on
|
|
21
|
+
// privatebanking.hsbc.com, <input type="text" role="region" readonly
|
|
22
|
+
// aria-readonly="true">. ARIA 1.2 does not give region aria-readonly, and
|
|
23
|
+
// Core-AAM maps the author's role, so the field reaches assistive
|
|
24
|
+
// technology as a landmark. (ARIA in HTML also forbids region on a text
|
|
25
|
+
// input, which is a role finding this rule does not make.) Decided from the
|
|
26
|
+
// spec text; Chromium's tree, which exposes the region with no readonly
|
|
27
|
+
// property, was the starting point and not the arbiter.
|
|
28
|
+
//
|
|
29
|
+
// aria-allowed-attr leaves these attributes to this rule, so an element gets
|
|
30
|
+
// one finding per attribute, not two. An element with a proven state and a
|
|
31
|
+
// second, unproven unsupported attribute gets one of each.
|
|
32
|
+
import { unsupportedAria, NATIVE_STATE } from './4.1.2-aria-allowed-attr.js';
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
id: 'aria-state-unreachable',
|
|
36
|
+
name: 'ARIA state the element really has cannot be reached',
|
|
37
|
+
impact: 'moderate',
|
|
38
|
+
tags: ['wcag2a', 'wcag412'],
|
|
39
|
+
help: 'A state the element has must be exposed through a role that supports it',
|
|
40
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
|
|
41
|
+
selector: '[aria-readonly], [aria-required], [aria-disabled], [aria-checked]',
|
|
42
|
+
visibleOnly: false,
|
|
43
|
+
evaluate(element) {
|
|
44
|
+
const found = unsupportedAria(element);
|
|
45
|
+
if (!found) return { status: 'pass' };
|
|
46
|
+
const proven = found.names.filter((name) => NATIVE_STATE[name] && element.hasAttribute(NATIVE_STATE[name]));
|
|
47
|
+
if (!proven.length) return { status: 'pass' };
|
|
48
|
+
const states = proven.map((name) => `aria-${name}`);
|
|
49
|
+
const natives = proven.map((name) => NATIVE_STATE[name]);
|
|
50
|
+
return {
|
|
51
|
+
status: 'fail',
|
|
52
|
+
message: `This element really is ${natives.join(' and ')}: its native ${natives.join(' and ')} attribute says so. But ${states.join(' and ')} is not supported on role "${found.role}" (ARIA 1.2 §8.6), so user agents drop it and assistive technology cannot learn the state. The control works, and nobody using a screen reader is told how.`,
|
|
53
|
+
fix: `Give the element a role that supports ${states.join(' and ')}, which for a form control is usually its own native role with no role attribute at all, or move the state to the element that carries the control's role. Do not add a role the host language forbids on this element to make the attribute legal.`,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
package/package.json
CHANGED