@vltpkg/query 1.0.0-rc.31 → 1.0.0-rc.32

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.
@@ -1,11 +1,15 @@
1
1
  import type { ParserState } from '../types.ts';
2
2
  import type { PostcssNode } from '@vltpkg/dss-parser';
3
3
  export type CveInternals = {
4
- cveId: string;
4
+ cveId: string | undefined;
5
5
  };
6
6
  export declare const parseInternals: (nodes: PostcssNode[]) => CveInternals;
7
7
  /**
8
- * Filters out any node that does not have a CVE alert with the specified CVE ID.
8
+ * Filters out any node that does not have a CVE alert.
9
+ *
10
+ * Usage:
11
+ * - :cve - matches any package with at least one CVE alert
12
+ * - :cve(CVE-2023-1234) - matches only the specific CVE ID
9
13
  */
10
14
  export declare const cve: (state: ParserState) => Promise<ParserState & {
11
15
  securityArchive: NonNullable<ParserState["securityArchive"]>;
@@ -2,23 +2,35 @@ import { error } from '@vltpkg/error-cause';
2
2
  import { asPostcssNodeWithChildren, asStringNode, asTagNode, isStringNode, isTagNode, } from '@vltpkg/dss-parser';
3
3
  import { assertSecurityArchive, removeDanglingEdges, removeNode, removeQuotes, } from "./helpers.js";
4
4
  export const parseInternals = (nodes) => {
5
+ if (!nodes[0]) {
6
+ return { cveId: undefined };
7
+ }
8
+ const selectorNode = asPostcssNodeWithChildren(nodes[0]);
9
+ if (!selectorNode.nodes[0]) {
10
+ return { cveId: undefined };
11
+ }
5
12
  let cveId = '';
6
- if (isStringNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])) {
7
- cveId = removeQuotes(asStringNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])
8
- .value);
13
+ if (isStringNode(selectorNode.nodes[0])) {
14
+ cveId = removeQuotes(asStringNode(selectorNode.nodes[0]).value);
9
15
  }
10
- else if (isTagNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])) {
11
- cveId = asTagNode(asPostcssNodeWithChildren(nodes[0]).nodes[0]).value;
16
+ else if (isTagNode(selectorNode.nodes[0])) {
17
+ cveId = asTagNode(selectorNode.nodes[0]).value;
12
18
  }
19
+ /* c8 ignore start - unreachable via normal parser */
13
20
  if (!cveId) {
14
21
  throw error('Expected a CVE ID', {
15
- found: asPostcssNodeWithChildren(nodes[0]).nodes[0],
22
+ found: selectorNode.nodes[0],
16
23
  });
17
24
  }
25
+ /* c8 ignore stop */
18
26
  return { cveId };
19
27
  };
20
28
  /**
21
- * Filters out any node that does not have a CVE alert with the specified CVE ID.
29
+ * Filters out any node that does not have a CVE alert.
30
+ *
31
+ * Usage:
32
+ * - :cve - matches any package with at least one CVE alert
33
+ * - :cve(CVE-2023-1234) - matches only the specific CVE ID
22
34
  */
23
35
  export const cve = async (state) => {
24
36
  assertSecurityArchive(state, 'cve');
@@ -26,14 +38,20 @@ export const cve = async (state) => {
26
38
  try {
27
39
  internals = parseInternals(asPostcssNodeWithChildren(state.current).nodes);
28
40
  }
29
- catch (err) {
41
+ catch (err) /* c8 ignore start */ {
30
42
  throw error('Failed to parse :cve selector', { cause: err });
31
- }
43
+ } /* c8 ignore stop */
32
44
  const { cveId } = internals;
33
45
  for (const node of state.partial.nodes) {
34
46
  const report = state.securityArchive.get(node.id);
35
- const exclude = !report?.alerts.some(alert => alert.props?.cveId?.trim().toLowerCase() ===
36
- cveId.trim().toLowerCase());
47
+ let exclude;
48
+ if (cveId === undefined) {
49
+ exclude = !report?.alerts.some(alert => alert.props?.cveId);
50
+ }
51
+ else {
52
+ exclude = !report?.alerts.some(alert => alert.props?.cveId?.trim().toLowerCase() ===
53
+ cveId.trim().toLowerCase());
54
+ }
37
55
  if (exclude) {
38
56
  removeNode(state, node);
39
57
  }
@@ -1,11 +1,15 @@
1
1
  import type { ParserState } from '../types.ts';
2
2
  import type { PostcssNode } from '@vltpkg/dss-parser';
3
3
  export type CweInternals = {
4
- cweId: string;
4
+ cweId: string | undefined;
5
5
  };
6
6
  export declare const parseInternals: (nodes: PostcssNode[]) => CweInternals;
7
7
  /**
8
- * Filters out any node that does not have a CWE alert with the specified CWE ID.
8
+ * Filters out any node that does not have a CWE alert.
9
+ *
10
+ * Usage:
11
+ * - :cwe - matches any package with at least one CWE alert
12
+ * - :cwe(CWE-79) - matches only the specific CWE ID
9
13
  */
10
14
  export declare const cwe: (state: ParserState) => Promise<ParserState & {
11
15
  securityArchive: NonNullable<ParserState["securityArchive"]>;
@@ -2,23 +2,35 @@ import { error } from '@vltpkg/error-cause';
2
2
  import { asPostcssNodeWithChildren, asStringNode, asTagNode, isStringNode, isTagNode, } from '@vltpkg/dss-parser';
3
3
  import { assertSecurityArchive, removeDanglingEdges, removeNode, removeQuotes, } from "./helpers.js";
4
4
  export const parseInternals = (nodes) => {
5
+ if (!nodes[0]) {
6
+ return { cweId: undefined };
7
+ }
8
+ const selectorNode = asPostcssNodeWithChildren(nodes[0]);
9
+ if (!selectorNode.nodes[0]) {
10
+ return { cweId: undefined };
11
+ }
5
12
  let cweId = '';
6
- if (isStringNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])) {
7
- cweId = removeQuotes(asStringNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])
8
- .value);
13
+ if (isStringNode(selectorNode.nodes[0])) {
14
+ cweId = removeQuotes(asStringNode(selectorNode.nodes[0]).value);
9
15
  }
10
- else if (isTagNode(asPostcssNodeWithChildren(nodes[0]).nodes[0])) {
11
- cweId = asTagNode(asPostcssNodeWithChildren(nodes[0]).nodes[0]).value;
16
+ else if (isTagNode(selectorNode.nodes[0])) {
17
+ cweId = asTagNode(selectorNode.nodes[0]).value;
12
18
  }
19
+ /* c8 ignore start - unreachable via normal parser */
13
20
  if (!cweId) {
14
21
  throw error('Expected a CWE ID', {
15
- found: asPostcssNodeWithChildren(nodes[0]).nodes[0],
22
+ found: selectorNode.nodes[0],
16
23
  });
17
24
  }
25
+ /* c8 ignore stop */
18
26
  return { cweId };
19
27
  };
20
28
  /**
21
- * Filters out any node that does not have a CWE alert with the specified CWE ID.
29
+ * Filters out any node that does not have a CWE alert.
30
+ *
31
+ * Usage:
32
+ * - :cwe - matches any package with at least one CWE alert
33
+ * - :cwe(CWE-79) - matches only the specific CWE ID
22
34
  */
23
35
  export const cwe = async (state) => {
24
36
  assertSecurityArchive(state, 'cwe');
@@ -26,13 +38,20 @@ export const cwe = async (state) => {
26
38
  try {
27
39
  internals = parseInternals(asPostcssNodeWithChildren(state.current).nodes);
28
40
  }
29
- catch (err) {
41
+ catch (err) /* c8 ignore start */ {
30
42
  throw error('Failed to parse :cwe selector', { cause: err });
31
- }
43
+ } /* c8 ignore stop */
32
44
  const { cweId } = internals;
33
45
  for (const node of state.partial.nodes) {
34
46
  const report = state.securityArchive.get(node.id);
35
- const exclude = !report?.alerts.some(alert => alert.props?.cwes?.some(cwe => cwe.id.trim().toLowerCase() === cweId.trim().toLowerCase()));
47
+ let exclude;
48
+ if (cweId === undefined) {
49
+ exclude = !report?.alerts.some(alert => alert.props?.cwes && alert.props.cwes.length > 0);
50
+ }
51
+ else {
52
+ exclude = !report?.alerts.some(alert => alert.props?.cwes?.some(cwe => cwe.id.trim().toLowerCase() ===
53
+ cweId.trim().toLowerCase()));
54
+ }
36
55
  if (exclude) {
37
56
  removeNode(state, node);
38
57
  }
@@ -0,0 +1,22 @@
1
+ import type { NodeLike } from '@vltpkg/types';
2
+ import type { ParserState } from '../types.ts';
3
+ /**
4
+ * Fetches the dist-tags of a package from the registry.
5
+ */
6
+ export declare const retrieveDistTags: (node: NodeLike, signal?: AbortSignal) => Promise<Record<string, string>>;
7
+ /**
8
+ * Checks whether a node's installed version matches the version
9
+ * associated with the given dist-tag. Returns the node if it should
10
+ * be removed (does NOT match), or undefined if it matches.
11
+ */
12
+ export declare const queueNode: (state: ParserState, node: NodeLike, tagName: string) => Promise<NodeLike | undefined>;
13
+ /**
14
+ * :dist(tag) Pseudo-Selector, matches only nodes whose installed
15
+ * version corresponds to the given dist-tag in the registry.
16
+ *
17
+ * Examples:
18
+ * - :dist(latest) — matches packages at the `latest` dist-tag version
19
+ * - :dist(nightly) — matches packages at the `nightly` dist-tag version
20
+ * - :not(:dist(nightly)) — excludes nightly-tagged versions
21
+ */
22
+ export declare const dist: (state: ParserState) => Promise<ParserState>;
@@ -0,0 +1,110 @@
1
+ import pRetry, { AbortError } from 'p-retry';
2
+ import { hydrate, splitDepID } from '@vltpkg/dep-id/browser';
3
+ import { error } from '@vltpkg/error-cause';
4
+ import { asPostcssNodeWithChildren, asTagNode, asStringNode, isTagNode, } from '@vltpkg/dss-parser';
5
+ import { removeDanglingEdges, removeNode, removeQuotes, } from "./helpers.js";
6
+ /**
7
+ * Fetches the dist-tags of a package from the registry.
8
+ */
9
+ export const retrieveDistTags = async (node, signal) => {
10
+ const spec = hydrate(node.id, String(node.name), node.options);
11
+ if (!spec.registry || !node.name) {
12
+ return {};
13
+ }
14
+ const url = new URL(spec.registry);
15
+ url.pathname = `/${node.name}`;
16
+ const response = await fetch(String(url), {
17
+ headers: {
18
+ Accept: 'application/vnd.npm.install-v1+json',
19
+ },
20
+ signal,
21
+ });
22
+ if (response.status === 404) {
23
+ throw new AbortError('Missing API');
24
+ }
25
+ if (!response.ok) {
26
+ throw error('Failed to fetch packument', {
27
+ name: node.name,
28
+ spec,
29
+ response,
30
+ });
31
+ }
32
+ const packument = (await response.json());
33
+ return packument['dist-tags'];
34
+ };
35
+ /**
36
+ * Checks whether a node's installed version matches the version
37
+ * associated with the given dist-tag. Returns the node if it should
38
+ * be removed (does NOT match), or undefined if it matches.
39
+ */
40
+ export const queueNode = async (state, node, tagName) => {
41
+ if (!node.name || !node.version) {
42
+ return node;
43
+ }
44
+ let distTags;
45
+ try {
46
+ distTags = await pRetry(() => retrieveDistTags(node, state.signal), {
47
+ retries: state.retries,
48
+ signal: state.signal,
49
+ });
50
+ }
51
+ catch (err) {
52
+ // eslint-disable-next-line no-console
53
+ console.warn(error('Could not retrieve dist-tags', {
54
+ name: node.name,
55
+ cause: err,
56
+ }));
57
+ return node;
58
+ }
59
+ const tagVersion = distTags[tagName];
60
+ if (tagVersion !== node.version) {
61
+ return node;
62
+ }
63
+ return undefined;
64
+ };
65
+ /**
66
+ * :dist(tag) Pseudo-Selector, matches only nodes whose installed
67
+ * version corresponds to the given dist-tag in the registry.
68
+ *
69
+ * Examples:
70
+ * - :dist(latest) — matches packages at the `latest` dist-tag version
71
+ * - :dist(nightly) — matches packages at the `nightly` dist-tag version
72
+ * - :not(:dist(nightly)) — excludes nightly-tagged versions
73
+ */
74
+ export const dist = async (state) => {
75
+ const top = asPostcssNodeWithChildren(state.current);
76
+ const selector = asPostcssNodeWithChildren(top.nodes[0]);
77
+ const firstChild = selector.nodes[0];
78
+ let tagName;
79
+ try {
80
+ tagName = removeQuotes(asStringNode(firstChild).value);
81
+ }
82
+ catch {
83
+ if (isTagNode(firstChild)) {
84
+ tagName = asTagNode(firstChild).value;
85
+ /* c8 ignore start */
86
+ }
87
+ else {
88
+ throw error('Failed to parse :dist selector', {
89
+ found: firstChild,
90
+ });
91
+ }
92
+ /* c8 ignore stop */
93
+ }
94
+ const queue = [];
95
+ for (const node of state.partial.nodes) {
96
+ if (splitDepID(node.id)[0] !== 'registry') {
97
+ removeNode(state, node);
98
+ continue;
99
+ }
100
+ queue.push(queueNode(state, node, tagName));
101
+ }
102
+ const removeNodeQueue = await Promise.all(queue);
103
+ for (const node of removeNodeQueue) {
104
+ if (node) {
105
+ removeNode(state, node);
106
+ }
107
+ }
108
+ removeDanglingEdges(state);
109
+ return state;
110
+ };
@@ -5,10 +5,12 @@ import type { PostcssNode } from '@vltpkg/dss-parser';
5
5
  export type SemverInternals = {
6
6
  semverValue: string;
7
7
  semverFunction: SemverComparatorFn;
8
+ semverRangeFunction: SemverRangeComparatorFn | undefined;
8
9
  compareAttribute: SemverCompareAttribute;
9
10
  };
10
- export type SemverFunctionNames = 'satisfies' | 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'neq';
11
+ export type SemverFunctionNames = 'satisfies' | 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'neq' | 'intersects' | 'subset';
11
12
  export type SemverComparatorFn = (version: Version | string, range: string) => boolean;
13
+ export type SemverRangeComparatorFn = (r1: string, r2: string) => boolean;
12
14
  export type SemverCompareAttribute = Pick<AttrInternals, 'attribute' | 'properties'> | undefined;
13
15
  export declare const isSemverFunctionName: (name: string) => name is SemverFunctionNames;
14
16
  export declare const asSemverFunctionName: (name: string) => SemverFunctionNames;
@@ -1,10 +1,10 @@
1
- import { satisfies, gt, gte, lt, lte, eq, neq, parse, parseRange, } from '@vltpkg/semver';
1
+ import { satisfies, gt, gte, lt, lte, eq, neq, intersects, subset, parse, parseRange, } from '@vltpkg/semver';
2
2
  import { error } from '@vltpkg/error-cause';
3
3
  import { asError } from '@vltpkg/types';
4
4
  import { parseInternals as parseAttrInternals } from "./attr.js";
5
5
  import { getManifestPropertyValues } from "../attribute.js";
6
6
  import { asAttributeNode, asPostcssNodeWithChildren, asPseudoNode, asStringNode, asTagNode, isAttributeNode, isPseudoNode, isStringNode, isTagNode, } from '@vltpkg/dss-parser';
7
- import { removeNode, removeQuotes } from "./helpers.js";
7
+ import { removeEdge, removeNode, removeQuotes, removeUnlinkedNodes, } from "./helpers.js";
8
8
  const semverFunctionNames = new Set([
9
9
  'satisfies',
10
10
  'gt',
@@ -13,6 +13,8 @@ const semverFunctionNames = new Set([
13
13
  'lte',
14
14
  'eq',
15
15
  'neq',
16
+ 'intersects',
17
+ 'subset',
16
18
  ]);
17
19
  export const isSemverFunctionName = (name) => semverFunctionNames.has(name);
18
20
  export const asSemverFunctionName = (name) => {
@@ -33,6 +35,10 @@ const semverFunctions = new Map([
33
35
  ['eq', eq],
34
36
  ['neq', neq],
35
37
  ]);
38
+ const semverRangeFunctions = new Map([
39
+ ['intersects', intersects],
40
+ ['subset', subset],
41
+ ]);
36
42
  export const parseInternals = (nodes, loose) => {
37
43
  // tries to parse the first param as a string node, otherwise defaults
38
44
  // to reading all postcss nodes as just strings, since it just means
@@ -82,13 +88,17 @@ export const parseInternals = (nodes, loose) => {
82
88
  }
83
89
  }
84
90
  const semverFunction = semverFunctions.get(fnName);
91
+ const semverRangeFunction = semverRangeFunctions.get(fnName);
85
92
  // the following should never happen as long as the semver function names
86
93
  // type and Set are correctly mirroring each other values
87
94
  /* c8 ignore start */
88
- if (!semverFunction) {
95
+ if (!semverFunction && !semverRangeFunction) {
89
96
  throw error('Invalid semver function name', {
90
97
  found: fnName,
91
- validOptions: Array.from(semverFunctions.keys()),
98
+ validOptions: [
99
+ ...Array.from(semverFunctions.keys()),
100
+ ...Array.from(semverRangeFunctions.keys()),
101
+ ],
92
102
  });
93
103
  }
94
104
  /* c8 ignore stop */
@@ -117,7 +127,8 @@ export const parseInternals = (nodes, loose) => {
117
127
  }
118
128
  return {
119
129
  semverValue,
120
- semverFunction,
130
+ semverFunction: semverFunction ?? satisfies,
131
+ semverRangeFunction,
121
132
  compareAttribute,
122
133
  };
123
134
  };
@@ -131,7 +142,33 @@ export const semverParser = async (state) => {
131
142
  cause: err,
132
143
  });
133
144
  }
134
- const { semverValue, semverFunction, compareAttribute } = internals;
145
+ const { semverValue, semverFunction, semverRangeFunction, compareAttribute, } = internals;
146
+ // Range-vs-range functions (subset, intersects) operate on edges by default
147
+ if (semverRangeFunction) {
148
+ if (compareAttribute) {
149
+ // Compare the semverValue against a manifest property value as ranges
150
+ for (const node of state.partial.nodes) {
151
+ const compareValues = getManifestPropertyValues(node, compareAttribute.properties, compareAttribute.attribute);
152
+ const compareValue = compareValues?.[0];
153
+ if (!compareValue ||
154
+ !semverRangeFunction(compareValue, semverValue)) {
155
+ removeNode(state, node);
156
+ }
157
+ }
158
+ }
159
+ else {
160
+ // Default: operate on edges using bareSpec
161
+ for (const edge of state.partial.edges) {
162
+ const edgeRange = edge.spec.semver ?? edge.spec.bareSpec;
163
+ if (!edgeRange ||
164
+ !semverRangeFunction(edgeRange, semverValue)) {
165
+ removeEdge(state, edge);
166
+ }
167
+ }
168
+ removeUnlinkedNodes(state);
169
+ }
170
+ return state;
171
+ }
135
172
  for (const node of state.partial.nodes) {
136
173
  if (compareAttribute) {
137
174
  const compareValues = getManifestPropertyValues(node, compareAttribute.properties, compareAttribute.attribute);
@@ -0,0 +1,8 @@
1
+ import type { ParserState } from '../types.ts';
2
+ /**
3
+ * :vulnerable / :vuln Pseudo-Selector matches any package version
4
+ * that has at least one CVE associated with it.
5
+ */
6
+ export declare const vulnerable: (state: ParserState) => Promise<ParserState & {
7
+ securityArchive: NonNullable<ParserState["securityArchive"]>;
8
+ }>;
@@ -0,0 +1,17 @@
1
+ import { assertSecurityArchive, removeDanglingEdges, removeNode, } from "./helpers.js";
2
+ /**
3
+ * :vulnerable / :vuln Pseudo-Selector matches any package version
4
+ * that has at least one CVE associated with it.
5
+ */
6
+ export const vulnerable = async (state) => {
7
+ assertSecurityArchive(state, 'vulnerable');
8
+ for (const node of state.partial.nodes) {
9
+ const report = state.securityArchive.get(node.id);
10
+ const hasCve = report?.alerts.some(alert => alert.props?.cveId);
11
+ if (!hasCve) {
12
+ removeNode(state, node);
13
+ }
14
+ }
15
+ removeDanglingEdges(state);
16
+ return state;
17
+ };
package/dist/pseudo.js CHANGED
@@ -12,6 +12,7 @@ import { debug } from "./pseudo/debug.js";
12
12
  import { deprecated } from "./pseudo/deprecated.js";
13
13
  import { dev } from "./pseudo/dev.js";
14
14
  import { diff } from "./pseudo/diff.js";
15
+ import { dist } from "./pseudo/dist.js";
15
16
  import { dynamic } from "./pseudo/dynamic.js";
16
17
  import { empty } from "./pseudo/empty.js";
17
18
  import { entropic } from "./pseudo/entropic.js";
@@ -57,6 +58,7 @@ import { unknown } from "./pseudo/unknown.js";
57
58
  import { unmaintained } from "./pseudo/unmaintained.js";
58
59
  import { unpopular } from "./pseudo/unpopular.js";
59
60
  import { unstable } from "./pseudo/unstable.js";
61
+ import { vulnerable } from "./pseudo/vulnerable.js";
60
62
  import { workspace } from "./pseudo/workspace.js";
61
63
  /**
62
64
  * :has Pseudo-Selector, matches only nodes that have valid results
@@ -286,6 +288,7 @@ const pseudoSelectors = new Map(Object.entries({
286
288
  deprecated,
287
289
  dev,
288
290
  diff,
291
+ dist,
289
292
  dynamic,
290
293
  eval: evalParser,
291
294
  empty,
@@ -338,6 +341,8 @@ const pseudoSelectors = new Map(Object.entries({
338
341
  unpopular,
339
342
  unstable,
340
343
  v: semver,
344
+ vuln: vulnerable,
345
+ vulnerable,
341
346
  workspace,
342
347
  }));
343
348
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/query",
3
3
  "description": "Query syntax parser that retrieves items from a graph",
4
- "version": "1.0.0-rc.31",
4
+ "version": "1.0.0-rc.32",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,12 +13,12 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/dep-id": "1.0.0-rc.31",
17
- "@vltpkg/dss-parser": "1.0.0-rc.31",
18
- "@vltpkg/error-cause": "1.0.0-rc.31",
19
- "@vltpkg/security-archive": "1.0.0-rc.31",
20
- "@vltpkg/semver": "1.0.0-rc.31",
21
- "@vltpkg/types": "1.0.0-rc.31",
16
+ "@vltpkg/dep-id": "1.0.0-rc.32",
17
+ "@vltpkg/dss-parser": "1.0.0-rc.32",
18
+ "@vltpkg/error-cause": "1.0.0-rc.32",
19
+ "@vltpkg/security-archive": "1.0.0-rc.32",
20
+ "@vltpkg/semver": "1.0.0-rc.32",
21
+ "@vltpkg/types": "1.0.0-rc.32",
22
22
  "minimatch": "^10.1.1",
23
23
  "p-retry": "^7.1.1",
24
24
  "postcss-selector-parser": "^7.1.1"
@@ -26,7 +26,7 @@
26
26
  "devDependencies": {
27
27
  "@eslint/js": "^9.39.1",
28
28
  "@types/node": "^22.19.2",
29
- "@vltpkg/spec": "1.0.0-rc.31",
29
+ "@vltpkg/spec": "1.0.0-rc.32",
30
30
  "eslint": "^9.39.1",
31
31
  "prettier": "^3.7.4",
32
32
  "tap": "^21.5.0",