@markuplint/rules 5.3.0 → 5.4.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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **rules:** do not report descendant attributes that take part in a text alternative ([46002a7](https://github.com/markuplint/markuplint/commit/46002a75e06f877938d134ce0958a625e5d989d3)), closes [#4408](https://github.com/markuplint/markuplint/issues/4408)
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
7
14
|
|
|
8
15
|
### Bug Fixes
|
|
@@ -13,7 +13,9 @@ export default createRule({
|
|
|
13
13
|
return;
|
|
14
14
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
15
15
|
return;
|
|
16
|
-
|
|
16
|
+
for (const attr of el.attributes) {
|
|
17
|
+
report(checkingPresentationalChildren({ attr, el }));
|
|
18
|
+
}
|
|
17
19
|
});
|
|
18
20
|
},
|
|
19
21
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Options } from '../types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AttrChecker, Element } from '@markuplint/ml-core';
|
|
3
3
|
/**
|
|
4
|
-
* Checks whether ARIA
|
|
5
|
-
* role has the `childrenArePresentational` characteristic.
|
|
4
|
+
* Checks whether an ARIA attribute or `role` is applied to a descendant of an
|
|
5
|
+
* element whose role has the `childrenArePresentational` characteristic.
|
|
6
6
|
*
|
|
7
7
|
* Presentational Children
|
|
8
8
|
*
|
|
@@ -29,7 +29,18 @@ import type { ElementChecker } from '@markuplint/ml-core';
|
|
|
29
29
|
* > can still be used in the name and description computation
|
|
30
30
|
* > even when not included in the accessibility tree.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Because the text of the excluded descendants is still used, an attribute that
|
|
33
|
+
* changes what a descendant contributes to a text alternative is not
|
|
34
|
+
* ineffective, and is not reported (see {@link takesEffect}). The computations
|
|
35
|
+
* that can read the descendant are listed by {@link getVisits};
|
|
36
|
+
* `MLElement#getAccessibleNameVisits` tells which elements each of them reaches
|
|
37
|
+
* and how.
|
|
38
|
+
*
|
|
39
|
+
* @param attr - The attribute to inspect.
|
|
40
|
+
* @param el - The element the attribute is read on. For a pretended component it is
|
|
41
|
+
* the component, while the attribute belongs to the element it pretends to be.
|
|
42
|
+
* @returns A violation if the attribute is an ARIA attribute or `role` that takes no effect under an ancestor with presentational children.
|
|
34
43
|
*/
|
|
35
|
-
export declare const checkingPresentationalChildren:
|
|
44
|
+
export declare const checkingPresentationalChildren: AttrChecker<boolean, Options, {
|
|
45
|
+
readonly el: Element<boolean, Options>;
|
|
46
|
+
}>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isAriaAttr } from '../../helpers.js';
|
|
2
|
-
import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
|
|
1
|
+
import { isAriaAttr, labelable, memoizeByObject, namespaceKeyOf } from '../../helpers.js';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION, EMBEDDED_CONTROL_ROLES, getComputedRole, getImplicitRole, isExposedAsPresentational, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
|
|
3
3
|
/**
|
|
4
|
-
* Checks whether ARIA
|
|
5
|
-
* role has the `childrenArePresentational` characteristic.
|
|
4
|
+
* Checks whether an ARIA attribute or `role` is applied to a descendant of an
|
|
5
|
+
* element whose role has the `childrenArePresentational` characteristic.
|
|
6
6
|
*
|
|
7
7
|
* Presentational Children
|
|
8
8
|
*
|
|
@@ -29,44 +29,257 @@ import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
|
|
|
29
29
|
* > can still be used in the name and description computation
|
|
30
30
|
* > even when not included in the accessibility tree.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Because the text of the excluded descendants is still used, an attribute that
|
|
33
|
+
* changes what a descendant contributes to a text alternative is not
|
|
34
|
+
* ineffective, and is not reported (see {@link takesEffect}). The computations
|
|
35
|
+
* that can read the descendant are listed by {@link getVisits};
|
|
36
|
+
* `MLElement#getAccessibleNameVisits` tells which elements each of them reaches
|
|
37
|
+
* and how.
|
|
38
|
+
*
|
|
39
|
+
* @param attr - The attribute to inspect.
|
|
40
|
+
* @param el - The element the attribute is read on. For a pretended component it is
|
|
41
|
+
* the component, while the attribute belongs to the element it pretends to be.
|
|
42
|
+
* @returns A violation if the attribute is an ARIA attribute or `role` that takes no effect under an ancestor with presentational children.
|
|
34
43
|
*/
|
|
35
|
-
export const checkingPresentationalChildren = ({ el }) => t => {
|
|
36
|
-
|
|
37
|
-
if (!ancestor) {
|
|
44
|
+
export const checkingPresentationalChildren = ({ attr, el }) => t => {
|
|
45
|
+
if (!isAriaAttr(attr) && attr.nameKey !== 'role') {
|
|
38
46
|
return;
|
|
39
47
|
}
|
|
40
|
-
|
|
48
|
+
const context = getContext(el);
|
|
49
|
+
if (!context) {
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
if (!hasAriaAttr) {
|
|
52
|
+
if (takesEffect(attr, el, context)) {
|
|
45
53
|
return;
|
|
46
54
|
}
|
|
47
55
|
return {
|
|
48
|
-
scope:
|
|
49
|
-
message: t('it may be ineffective because {0}', t("it has {0} as an ancestor that doesn't expose its descendants to the accessibility tree", t('the "{0*}" {1}',
|
|
56
|
+
scope: attr,
|
|
57
|
+
message: t('it may be ineffective because {0}', t("it has {0} as an ancestor that doesn't expose its descendants to the accessibility tree", t('the "{0*}" {1}', context.role.name, 'role'))),
|
|
50
58
|
};
|
|
51
59
|
};
|
|
52
60
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
61
|
+
* The closest ancestor role with presentational children, and the visits of the
|
|
62
|
+
* element by the computations that can read it, once per element.
|
|
63
|
+
*/
|
|
64
|
+
const getContext = memoizeByObject(
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
66
|
+
(el) => {
|
|
67
|
+
const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
68
|
+
const role = getClosestPresentationalChildrenRole(el, ariaVersion);
|
|
69
|
+
if (!role) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return { ariaVersion, role, visits: getVisits(el, ariaVersion) };
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Finds the closest ancestor whose computed role has the
|
|
76
|
+
* `childrenArePresentational` characteristic.
|
|
55
77
|
*
|
|
56
78
|
* @param el - The element to start searching from (exclusive, starts from parent).
|
|
57
|
-
* @
|
|
79
|
+
* @param ariaVersion - The ARIA version to compute the roles with.
|
|
80
|
+
* @returns The role of that ancestor, or `null` if none is found.
|
|
58
81
|
*/
|
|
59
|
-
function
|
|
82
|
+
function getClosestPresentationalChildrenRole(
|
|
60
83
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
61
|
-
el) {
|
|
62
|
-
const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
84
|
+
el, ariaVersion) {
|
|
63
85
|
let current = el.parentElement;
|
|
64
86
|
while (current) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
87
|
+
const role = getComputedRole(el.ownerMLDocument.specs, current, ariaVersion).role;
|
|
88
|
+
if (role?.childrenPresentational) {
|
|
89
|
+
return role;
|
|
68
90
|
}
|
|
69
91
|
current = current.parentElement;
|
|
70
92
|
}
|
|
71
93
|
return null;
|
|
72
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The visits of the element by every computation that can read it. Each reader
|
|
97
|
+
* is taken whether or not its computation reaches the element; one that does
|
|
98
|
+
* not contributes no visit.
|
|
99
|
+
*
|
|
100
|
+
* - the accessible name of every ancestor, as any of them can collect the text
|
|
101
|
+
* of its subtree (Step 2F and the recursion into descendants)
|
|
102
|
+
* - the text alternative of the element or of an ancestor as a node that
|
|
103
|
+
* `aria-labelledby` or `aria-describedby` refers to
|
|
104
|
+
* - the accessible name of every control that a `label` ancestor can name: the
|
|
105
|
+
* element its `for` refers to, and its labelable descendants (HTML-AAM)
|
|
106
|
+
*
|
|
107
|
+
* @see https://w3c.github.io/accname/#comp_name_from_content
|
|
108
|
+
* @see https://w3c.github.io/accname/#comp_labelledby_traversal
|
|
109
|
+
* @see https://w3c.github.io/html-aam/#accessible-name-and-description-computation
|
|
110
|
+
*
|
|
111
|
+
* @param el - The descendant
|
|
112
|
+
* @param ariaVersion - The ARIA version to compute the names with
|
|
113
|
+
* @returns The visits
|
|
114
|
+
*/
|
|
115
|
+
function getVisits(
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
117
|
+
el, ariaVersion) {
|
|
118
|
+
const visits = [];
|
|
119
|
+
const add = (
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
121
|
+
reader, asReferenced = false) => {
|
|
122
|
+
visits.push(...(reader.getAccessibleNameVisits(ariaVersion, asReferenced).get(el) ?? []));
|
|
123
|
+
};
|
|
124
|
+
const document = el.ownerMLDocument;
|
|
125
|
+
const referenced = getReferencedIds(document);
|
|
126
|
+
let current = el;
|
|
127
|
+
while (current) {
|
|
128
|
+
if (current !== el) {
|
|
129
|
+
add(current);
|
|
130
|
+
}
|
|
131
|
+
if (current.id && referenced.has(current.id) && document.getElementById(current.id) === current) {
|
|
132
|
+
add(current, true);
|
|
133
|
+
}
|
|
134
|
+
for (const control of getLabeledControls(current)) {
|
|
135
|
+
add(control);
|
|
136
|
+
}
|
|
137
|
+
current = current.parentElement;
|
|
138
|
+
}
|
|
139
|
+
return visits;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The controls that the element can name if it is a `label`, as the accessible
|
|
143
|
+
* name computation finds labels: by `for` or as the closest `label` ancestor.
|
|
144
|
+
* Once per label.
|
|
145
|
+
*
|
|
146
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
147
|
+
*/
|
|
148
|
+
const getLabeledControls = memoizeByObject(
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
150
|
+
(el) => {
|
|
151
|
+
if (el.localName !== 'label' || el.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const htmlFor = el.getAttribute('for');
|
|
155
|
+
const target = htmlFor == null ? null : el.ownerMLDocument.getElementById(htmlFor);
|
|
156
|
+
return [...(target ? [target] : []), ...el.querySelectorAll(labelable.join(', '))];
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* The IDs that an `aria-labelledby` or `aria-describedby` of the document refers to.
|
|
160
|
+
*/
|
|
161
|
+
const getReferencedIds = memoizeByObject(
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
163
|
+
(document) => {
|
|
164
|
+
const ids = new Set();
|
|
165
|
+
for (const referrer of document.querySelectorAll('[aria-labelledby], [aria-describedby]')) {
|
|
166
|
+
for (const name of ['aria-labelledby', 'aria-describedby']) {
|
|
167
|
+
for (const id of referrer.getAttribute(name)?.split(/\s+/) ?? []) {
|
|
168
|
+
if (id !== '') {
|
|
169
|
+
ids.add(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return ids;
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* Whether the attribute changes what its element contributes to a text
|
|
178
|
+
* alternative that reads it, given how the computations reach the element.
|
|
179
|
+
*
|
|
180
|
+
* - `aria-hidden="true"` makes the element hidden, so it and its subtree
|
|
181
|
+
* contribute nothing (Step 2A), unless the `hidden` attribute hides it already
|
|
182
|
+
* (the other way `@markuplint/ml-core` tells an element hidden).
|
|
183
|
+
* - An `aria-labelledby` replaces the element's subtree (Step 2B) when the
|
|
184
|
+
* elements it refers to have any text, except inside an ongoing
|
|
185
|
+
* `aria-labelledby` traversal.
|
|
186
|
+
* - A non-empty `aria-label` replaces it the same way (Step 2D), unless the
|
|
187
|
+
* element is hidden, an `aria-labelledby` of it takes effect first (one that
|
|
188
|
+
* refers to the element itself reads the `aria-label` again), or it is an
|
|
189
|
+
* embedded control, whose `aria-label` the recursion ignores.
|
|
190
|
+
* - `role="none"` or `role="presentation"` drops the host language text
|
|
191
|
+
* alternative of the element, such as the `alt` of an image (Step 2E), when the
|
|
192
|
+
* element has one and the Presentational Roles Conflict Resolution does not
|
|
193
|
+
* ignore the role.
|
|
194
|
+
* - A role that makes the element an embedded control, or keeps a native one
|
|
195
|
+
* from being it, changes what it contributes between its value and its text
|
|
196
|
+
* (Step 2C).
|
|
197
|
+
* - The `aria-valuetext`, or else the `aria-valuenow`, of a range embedded
|
|
198
|
+
* control is its value (Step 2C).
|
|
199
|
+
*
|
|
200
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control
|
|
201
|
+
* @see https://w3c.github.io/accname/#comp_hidden_not_referenced
|
|
202
|
+
* @see https://w3c.github.io/accname/#comp_labelledby
|
|
203
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
204
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
205
|
+
*
|
|
206
|
+
* > Otherwise, if the current node's native markup provides an attribute (e.g. alt) or element (e.g. HTML label or SVG title) that defines a text alternative, return that alternative in the form of a flat string as defined by the host language […], unless the current node is exposed as presentational (role="presentation" or role="none", alt="").
|
|
207
|
+
*
|
|
208
|
+
* @param attr - The attribute on a descendant of a presentational ancestor
|
|
209
|
+
* @param el - The descendant
|
|
210
|
+
* @param context - The ARIA version and the visits of the descendant
|
|
211
|
+
* @returns `true` if the attribute takes effect on a text alternative
|
|
212
|
+
*/
|
|
213
|
+
function takesEffect(
|
|
214
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
215
|
+
attr,
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
217
|
+
el, { ariaVersion, visits }) {
|
|
218
|
+
const shown = visits.filter(visit => !visit.hidden);
|
|
219
|
+
switch (namespaceKeyOf(attr)) {
|
|
220
|
+
case 'aria-hidden': {
|
|
221
|
+
return attr.value === 'true' && visits.some(visit => visit.hidden) && !el.hasAttribute('hidden');
|
|
222
|
+
}
|
|
223
|
+
case 'aria-labelledby': {
|
|
224
|
+
return labelledbyGivesText(el, ariaVersion) && shown.some(visit => !visit.inLabelledbyTraversal);
|
|
225
|
+
}
|
|
226
|
+
case 'aria-label': {
|
|
227
|
+
const overridden = labelledbyGivesText(el, ariaVersion) &&
|
|
228
|
+
!(el.id !== '' && el.getAttribute('aria-labelledby')?.split(/\s+/).includes(el.id));
|
|
229
|
+
return (attr.value.trim() !== '' &&
|
|
230
|
+
shown.some(visit => !visit.embeddedControl && (visit.inLabelledbyTraversal || !overridden)));
|
|
231
|
+
}
|
|
232
|
+
case 'aria-valuetext':
|
|
233
|
+
case 'aria-valuenow': {
|
|
234
|
+
// The value of a range embedded control: `aria-valuetext`, else `aria-valuenow`
|
|
235
|
+
const isRange = ['slider', 'spinbutton'].includes(el.getAttribute('role')?.trim().split(/\s+/)[0] ?? '') ||
|
|
236
|
+
(el.localName === 'input' && el.getAttribute('type')?.trim().toLowerCase() === 'range');
|
|
237
|
+
const valuetext = el.getAttribute('aria-valuetext')?.trim() ?? '';
|
|
238
|
+
const shadowed = namespaceKeyOf(attr) === 'aria-valuenow' && valuetext !== '';
|
|
239
|
+
// Step 2B comes first outside an ongoing aria-labelledby traversal
|
|
240
|
+
const labelledby = labelledbyGivesText(el, ariaVersion);
|
|
241
|
+
return (isRange &&
|
|
242
|
+
attr.value.trim() !== '' &&
|
|
243
|
+
!shadowed &&
|
|
244
|
+
shown.some(visit => visit.embeddedControl && (visit.inLabelledbyTraversal || !labelledby)));
|
|
245
|
+
}
|
|
246
|
+
case 'role': {
|
|
247
|
+
if (shown.length === 0) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const { specs } = el.ownerMLDocument;
|
|
251
|
+
const computed = getComputedRole(specs, el, ariaVersion);
|
|
252
|
+
if (isEmbeddedControl(el, computed) !== isEmbeddedControl(el, getImplicitRole(specs, el, ariaVersion))) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return (shown.some(visit => !visit.embeddedControl) &&
|
|
256
|
+
isExposedAsPresentational(computed) &&
|
|
257
|
+
el.getHostLanguageLabelSource(ariaVersion) !== null);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Whether the element is an embedded control with the role, as the resolver of
|
|
264
|
+
* `@markuplint/ml-core` tells it: by the role, or as a native control without one.
|
|
265
|
+
*/
|
|
266
|
+
function isEmbeddedControl(
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
268
|
+
el, computed) {
|
|
269
|
+
return computed.role ? EMBEDDED_CONTROL_ROLES.has(computed.role.name) : isNativeEmbeddedControl(el);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Whether the `aria-labelledby` of the element gives a text: Step 2B returns the
|
|
273
|
+
* accumulated text of the elements it refers to only when that text is not empty.
|
|
274
|
+
*/
|
|
275
|
+
function labelledbyGivesText(
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
277
|
+
el, ariaVersion) {
|
|
278
|
+
return (el.getAttribute('aria-labelledby')?.split(/\s+/) ?? []).some(id => {
|
|
279
|
+
if (id === '') {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const referenced = el.ownerMLDocument.getElementById(id);
|
|
283
|
+
return referenced != null && referenced.getReferencedTextAlternative(ariaVersion) !== '';
|
|
284
|
+
});
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/rules",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Built-in rules of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"./lib/permitted-contents/debug.js": "./lib/permitted-contents/debug.browser.js"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@markuplint/html-spec": "5.
|
|
35
|
-
"@markuplint/ml-config": "5.
|
|
36
|
-
"@markuplint/ml-core": "5.
|
|
37
|
-
"@markuplint/ml-spec": "5.
|
|
38
|
-
"@markuplint/selector": "5.
|
|
39
|
-
"@markuplint/shared": "5.
|
|
40
|
-
"@markuplint/types": "5.
|
|
41
|
-
"@mdn/browser-compat-data": "^8.1.
|
|
34
|
+
"@markuplint/html-spec": "5.4.0",
|
|
35
|
+
"@markuplint/ml-config": "5.4.0",
|
|
36
|
+
"@markuplint/ml-core": "5.4.0",
|
|
37
|
+
"@markuplint/ml-spec": "5.4.0",
|
|
38
|
+
"@markuplint/selector": "5.4.0",
|
|
39
|
+
"@markuplint/shared": "5.4.0",
|
|
40
|
+
"@markuplint/types": "5.4.0",
|
|
41
|
+
"@mdn/browser-compat-data": "^8.1.6",
|
|
42
42
|
"@types/debug": "^4.1.13",
|
|
43
43
|
"ansi-colors": "^4.1.3",
|
|
44
44
|
"browserslist": "^4.29.3",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"image-size": "^2.0.4",
|
|
48
48
|
"type-fest": "^5.10.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
|
|
51
51
|
}
|