miniread 1.36.0 → 1.38.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/dist/transforms/_generated/manifest.js +12 -0
- package/dist/transforms/_generated/registry.js +4 -0
- package/dist/transforms/rename-timeout-duration-parameters/manifest.json +6 -0
- package/dist/transforms/rename-timeout-duration-parameters/rename-timeout-duration-parameters-transform.d.ts +2 -0
- package/dist/transforms/rename-timeout-duration-parameters/rename-timeout-duration-parameters-transform.js +137 -0
- package/dist/transforms/rename-url-parameters/rename-url-parameters-transform.js +1 -50
- package/dist/transforms/rename-url-variables/manifest.json +6 -0
- package/dist/transforms/rename-url-variables/rename-url-variables-transform.d.ts +2 -0
- package/dist/transforms/rename-url-variables/rename-url-variables-transform.js +94 -0
- package/dist/transforms/url-usage-heuristics.d.ts +3 -0
- package/dist/transforms/url-usage-heuristics.js +50 -0
- package/package.json +1 -1
|
@@ -192,6 +192,12 @@ const manifestData = {
|
|
|
192
192
|
evaluatedAt: "2026-01-23T17:57:26.908Z",
|
|
193
193
|
notes: "Measured with baseline none: 0.00%. Improves readability by stabilizing `this` aliases used for closures.",
|
|
194
194
|
},
|
|
195
|
+
"rename-timeout-duration-parameters": {
|
|
196
|
+
diffReductionImpact: -0.000018848364904400228,
|
|
197
|
+
recommended: false,
|
|
198
|
+
evaluatedAt: "2026-01-24T23:36:14.192Z",
|
|
199
|
+
notes: "Negative diff reduction; leaving out of recommended preset.",
|
|
200
|
+
},
|
|
195
201
|
"rename-timeout-ids": {
|
|
196
202
|
diffReductionImpact: 0.00003774938185385768,
|
|
197
203
|
recommended: true,
|
|
@@ -214,6 +220,12 @@ const manifestData = {
|
|
|
214
220
|
evaluatedAt: "2026-01-24T15:52:58.710Z",
|
|
215
221
|
notes: "Measured with baseline none: 0.00%. Added to recommended for readability.",
|
|
216
222
|
},
|
|
223
|
+
"rename-url-variables": {
|
|
224
|
+
diffReductionImpact: 0.000018848364904289205,
|
|
225
|
+
recommended: true,
|
|
226
|
+
evaluatedAt: "2026-01-24T23:50:56.857Z",
|
|
227
|
+
notes: "Measured with baseline none: 0.00%. Added to recommended for readability.",
|
|
228
|
+
},
|
|
217
229
|
"rename-use-reference-guards": {
|
|
218
230
|
diffReductionImpact: 0,
|
|
219
231
|
recommended: false,
|
|
@@ -32,10 +32,12 @@ import { renameRegexBuildersTransform } from "../rename-regex-builders/rename-re
|
|
|
32
32
|
import { renameReplaceChildParametersTransform } from "../rename-replace-child-parameters/rename-replace-child-parameters-transform.js";
|
|
33
33
|
import { renameRestParametersTransform } from "../rename-rest-parameters/rename-rest-parameters-transform.js";
|
|
34
34
|
import { renameThisAliasesTransform } from "../rename-this-aliases/rename-this-aliases-transform.js";
|
|
35
|
+
import { renameTimeoutDurationParametersTransform } from "../rename-timeout-duration-parameters/rename-timeout-duration-parameters-transform.js";
|
|
35
36
|
import { renameTimeoutIdsTransform } from "../rename-timeout-ids/rename-timeout-ids-transform.js";
|
|
36
37
|
import { renameTypeofVariablesTransform } from "../rename-typeof-variables/rename-typeof-variables-transform.js";
|
|
37
38
|
import { renameUint8arrayConcatVariablesTransform } from "../rename-uint8array-concat-variables/rename-uint8array-concat-variables-transform.js";
|
|
38
39
|
import { renameUrlParametersTransform } from "../rename-url-parameters/rename-url-parameters-transform.js";
|
|
40
|
+
import { renameUrlVariablesTransform } from "../rename-url-variables/rename-url-variables-transform.js";
|
|
39
41
|
import { renameUseReferenceGuardsTransform } from "../rename-use-reference-guards/rename-use-reference-guards-transform.js";
|
|
40
42
|
import { renameUseReferenceGuardsV2Transform } from "../rename-use-reference-guards-v2/rename-use-reference-guards-v2-transform.js";
|
|
41
43
|
import { simplifyBooleanNegationsTransform } from "../simplify-boolean-negations/simplify-boolean-negations-transform.js";
|
|
@@ -75,10 +77,12 @@ export const transformRegistry = {
|
|
|
75
77
|
[renameReplaceChildParametersTransform.id]: renameReplaceChildParametersTransform,
|
|
76
78
|
[renameRestParametersTransform.id]: renameRestParametersTransform,
|
|
77
79
|
[renameThisAliasesTransform.id]: renameThisAliasesTransform,
|
|
80
|
+
[renameTimeoutDurationParametersTransform.id]: renameTimeoutDurationParametersTransform,
|
|
78
81
|
[renameTimeoutIdsTransform.id]: renameTimeoutIdsTransform,
|
|
79
82
|
[renameTypeofVariablesTransform.id]: renameTypeofVariablesTransform,
|
|
80
83
|
[renameUint8arrayConcatVariablesTransform.id]: renameUint8arrayConcatVariablesTransform,
|
|
81
84
|
[renameUrlParametersTransform.id]: renameUrlParametersTransform,
|
|
85
|
+
[renameUrlVariablesTransform.id]: renameUrlVariablesTransform,
|
|
82
86
|
[renameUseReferenceGuardsTransform.id]: renameUseReferenceGuardsTransform,
|
|
83
87
|
[renameUseReferenceGuardsV2Transform.id]: renameUseReferenceGuardsV2Transform,
|
|
84
88
|
[simplifyBooleanNegationsTransform.id]: simplifyBooleanNegationsTransform,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
|
|
3
|
+
import { getFilesToProcess, } from "../../core/types.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
6
|
+
const traverse = require("@babel/traverse").default;
|
|
7
|
+
const BASE_NAME = "timeoutMs";
|
|
8
|
+
const getParameterIdentifier = (parameter) => {
|
|
9
|
+
if (parameter.type === "Identifier")
|
|
10
|
+
return parameter;
|
|
11
|
+
if (parameter.type !== "AssignmentPattern")
|
|
12
|
+
return undefined;
|
|
13
|
+
const { left } = parameter;
|
|
14
|
+
if (left.type !== "Identifier")
|
|
15
|
+
return undefined;
|
|
16
|
+
return left;
|
|
17
|
+
};
|
|
18
|
+
const isSetTimeoutDelayReference = (referencePath, bindingName) => {
|
|
19
|
+
if (!referencePath.isIdentifier())
|
|
20
|
+
return false;
|
|
21
|
+
const callPath = referencePath.parentPath;
|
|
22
|
+
if (!callPath.isCallExpression())
|
|
23
|
+
return false;
|
|
24
|
+
if (referencePath.listKey !== "arguments")
|
|
25
|
+
return false;
|
|
26
|
+
if (typeof referencePath.key !== "number")
|
|
27
|
+
return false;
|
|
28
|
+
if (referencePath.key !== 1)
|
|
29
|
+
return false;
|
|
30
|
+
const call = callPath.node;
|
|
31
|
+
if (call.callee.type !== "Identifier")
|
|
32
|
+
return false;
|
|
33
|
+
if (call.callee.name !== "setTimeout")
|
|
34
|
+
return false;
|
|
35
|
+
if (callPath.scope.hasBinding("setTimeout", true))
|
|
36
|
+
return false;
|
|
37
|
+
return referencePath.node.name === bindingName;
|
|
38
|
+
};
|
|
39
|
+
const isIfTestReference = (referencePath) => {
|
|
40
|
+
if (!referencePath.isIdentifier())
|
|
41
|
+
return false;
|
|
42
|
+
const parentPath = referencePath.parentPath;
|
|
43
|
+
if (parentPath.isIfStatement()) {
|
|
44
|
+
return parentPath.node.test === referencePath.node;
|
|
45
|
+
}
|
|
46
|
+
if (parentPath.isUnaryExpression({ operator: "!" })) {
|
|
47
|
+
const ifPath = parentPath.parentPath;
|
|
48
|
+
if (!ifPath.isIfStatement())
|
|
49
|
+
return false;
|
|
50
|
+
if (ifPath.node.test !== parentPath.node)
|
|
51
|
+
return false;
|
|
52
|
+
return parentPath.node.argument === referencePath.node;
|
|
53
|
+
}
|
|
54
|
+
if (parentPath.isBinaryExpression()) {
|
|
55
|
+
const ifPath = parentPath.parentPath;
|
|
56
|
+
if (!ifPath.isIfStatement())
|
|
57
|
+
return false;
|
|
58
|
+
if (ifPath.node.test !== parentPath.node)
|
|
59
|
+
return false;
|
|
60
|
+
return (parentPath.node.left === referencePath.node ||
|
|
61
|
+
parentPath.node.right === referencePath.node);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
const isLetter = (char) => /^[a-z]$/iu.test(char);
|
|
66
|
+
const containsMsUnit = (raw) => {
|
|
67
|
+
let index = raw.indexOf("ms");
|
|
68
|
+
while (index !== -1) {
|
|
69
|
+
const before = raw[index - 1];
|
|
70
|
+
const after = raw[index + 2];
|
|
71
|
+
const beforeOk = before === undefined || !isLetter(before);
|
|
72
|
+
const afterOk = after === undefined || !isLetter(after);
|
|
73
|
+
if (beforeOk && afterOk)
|
|
74
|
+
return true;
|
|
75
|
+
index = raw.indexOf("ms", index + 2);
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
const isTimeoutTemplateReference = (referencePath) => {
|
|
80
|
+
if (!referencePath.isIdentifier())
|
|
81
|
+
return false;
|
|
82
|
+
const parentPath = referencePath.parentPath;
|
|
83
|
+
if (!parentPath.isTemplateLiteral())
|
|
84
|
+
return false;
|
|
85
|
+
const template = parentPath.node;
|
|
86
|
+
return template.quasis.some((quasi) => containsMsUnit(quasi.value.raw));
|
|
87
|
+
};
|
|
88
|
+
const isAllowedTimeoutReference = (referencePath, bindingName) => {
|
|
89
|
+
return (isSetTimeoutDelayReference(referencePath, bindingName) ||
|
|
90
|
+
isIfTestReference(referencePath) ||
|
|
91
|
+
isTimeoutTemplateReference(referencePath));
|
|
92
|
+
};
|
|
93
|
+
export const renameTimeoutDurationParametersTransform = {
|
|
94
|
+
id: "rename-timeout-duration-parameters",
|
|
95
|
+
description: "Renames timeout duration parameters to $timeoutMs when used for setTimeout delays and timeout messages",
|
|
96
|
+
scope: "file",
|
|
97
|
+
parallelizable: true,
|
|
98
|
+
transform(context) {
|
|
99
|
+
let nodesVisited = 0;
|
|
100
|
+
let transformationsApplied = 0;
|
|
101
|
+
for (const fileInfo of getFilesToProcess(context)) {
|
|
102
|
+
const group = new RenameGroup();
|
|
103
|
+
traverse(fileInfo.ast, {
|
|
104
|
+
Function(path) {
|
|
105
|
+
nodesVisited++;
|
|
106
|
+
for (const parameter of path.node.params) {
|
|
107
|
+
const identifier = getParameterIdentifier(parameter);
|
|
108
|
+
if (!identifier)
|
|
109
|
+
continue;
|
|
110
|
+
if (isStableRenamed(identifier.name))
|
|
111
|
+
continue;
|
|
112
|
+
const binding = path.scope.getBinding(identifier.name);
|
|
113
|
+
if (!binding)
|
|
114
|
+
continue;
|
|
115
|
+
if (binding.kind !== "param")
|
|
116
|
+
continue;
|
|
117
|
+
if (!binding.constant)
|
|
118
|
+
continue;
|
|
119
|
+
if (binding.referencePaths.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
if (!binding.referencePaths.some((referencePath) => isSetTimeoutDelayReference(referencePath, identifier.name)))
|
|
122
|
+
continue;
|
|
123
|
+
if (!binding.referencePaths.every((referencePath) => isAllowedTimeoutReference(referencePath, identifier.name)))
|
|
124
|
+
continue;
|
|
125
|
+
group.add({
|
|
126
|
+
scope: path.scope,
|
|
127
|
+
currentName: identifier.name,
|
|
128
|
+
baseName: BASE_NAME,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
transformationsApplied += group.apply();
|
|
134
|
+
}
|
|
135
|
+
return Promise.resolve({ nodesVisited, transformationsApplied });
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -2,59 +2,10 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { isIdentifierName, isKeyword, isStrictBindReservedWord, } from "@babel/helper-validator-identifier";
|
|
3
3
|
import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
|
|
4
4
|
import { getFilesToProcess, } from "../../core/types.js";
|
|
5
|
+
import { hasUrlDestructure } from "../url-usage-heuristics.js";
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
7
8
|
const traverse = require("@babel/traverse").default;
|
|
8
|
-
const urlPropertyNames = new Set([
|
|
9
|
-
"hash",
|
|
10
|
-
"host",
|
|
11
|
-
"hostname",
|
|
12
|
-
"href",
|
|
13
|
-
"origin",
|
|
14
|
-
"password",
|
|
15
|
-
"pathname",
|
|
16
|
-
"port",
|
|
17
|
-
"protocol",
|
|
18
|
-
"search",
|
|
19
|
-
"searchParams",
|
|
20
|
-
"username",
|
|
21
|
-
]);
|
|
22
|
-
const countUrlProperties = (pattern) => {
|
|
23
|
-
let count = 0;
|
|
24
|
-
for (const property of pattern.properties) {
|
|
25
|
-
if (property.type !== "ObjectProperty")
|
|
26
|
-
continue;
|
|
27
|
-
if (property.computed)
|
|
28
|
-
continue;
|
|
29
|
-
const key = property.key;
|
|
30
|
-
const keyName = key.type === "Identifier"
|
|
31
|
-
? key.name
|
|
32
|
-
: key.type === "StringLiteral"
|
|
33
|
-
? key.value
|
|
34
|
-
: undefined;
|
|
35
|
-
if (keyName && urlPropertyNames.has(keyName)) {
|
|
36
|
-
count += 1;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return count;
|
|
40
|
-
};
|
|
41
|
-
const hasUrlDestructure = (referencePath) => {
|
|
42
|
-
const parent = referencePath.parentPath;
|
|
43
|
-
if (!parent)
|
|
44
|
-
return false;
|
|
45
|
-
if (parent.isVariableDeclarator() &&
|
|
46
|
-
parent.node.init === referencePath.node &&
|
|
47
|
-
parent.node.id.type === "ObjectPattern") {
|
|
48
|
-
return countUrlProperties(parent.node.id) >= 2;
|
|
49
|
-
}
|
|
50
|
-
if (parent.isAssignmentExpression() &&
|
|
51
|
-
parent.node.operator === "=" &&
|
|
52
|
-
parent.node.right === referencePath.node &&
|
|
53
|
-
parent.node.left.type === "ObjectPattern") {
|
|
54
|
-
return countUrlProperties(parent.node.left) >= 2;
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
};
|
|
58
9
|
const hasUrlConstruction = (referencePath) => {
|
|
59
10
|
const parent = referencePath.parentPath;
|
|
60
11
|
if (!parent?.isNewExpression())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
|
|
3
|
+
import { getFilesToProcess, } from "../../core/types.js";
|
|
4
|
+
import { hasUrlDestructure, urlPropertyNames, } from "../url-usage-heuristics.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
7
|
+
const traverse = require("@babel/traverse").default;
|
|
8
|
+
const isUrlConstruction = (node) => {
|
|
9
|
+
if (node?.type !== "NewExpression")
|
|
10
|
+
return false;
|
|
11
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "URL") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
const isUrlPropertyAccess = (referencePath) => {
|
|
17
|
+
const parent = referencePath.parentPath;
|
|
18
|
+
if (!parent)
|
|
19
|
+
return false;
|
|
20
|
+
if (!parent.isMemberExpression())
|
|
21
|
+
return false;
|
|
22
|
+
if (parent.node.object !== referencePath.node)
|
|
23
|
+
return false;
|
|
24
|
+
if (parent.node.computed)
|
|
25
|
+
return false;
|
|
26
|
+
const key = parent.node.property;
|
|
27
|
+
const keyName = key.type === "Identifier"
|
|
28
|
+
? key.name
|
|
29
|
+
: key.type === "StringLiteral"
|
|
30
|
+
? key.value
|
|
31
|
+
: undefined;
|
|
32
|
+
if (!keyName)
|
|
33
|
+
return false;
|
|
34
|
+
return urlPropertyNames.has(keyName);
|
|
35
|
+
};
|
|
36
|
+
export const renameUrlVariablesTransform = {
|
|
37
|
+
id: "rename-url-variables",
|
|
38
|
+
description: "Renames variables initialized with new URL(...) when they are accessed via URL properties",
|
|
39
|
+
scope: "file",
|
|
40
|
+
parallelizable: true,
|
|
41
|
+
transform(context) {
|
|
42
|
+
let nodesVisited = 0;
|
|
43
|
+
let transformationsApplied = 0;
|
|
44
|
+
for (const fileInfo of getFilesToProcess(context)) {
|
|
45
|
+
const group = new RenameGroup();
|
|
46
|
+
traverse(fileInfo.ast, {
|
|
47
|
+
VariableDeclarator(path) {
|
|
48
|
+
nodesVisited += 1;
|
|
49
|
+
if (path.node.id.type !== "Identifier")
|
|
50
|
+
return;
|
|
51
|
+
if (!isUrlConstruction(path.node.init))
|
|
52
|
+
return;
|
|
53
|
+
const currentName = path.node.id.name;
|
|
54
|
+
if (isStableRenamed(currentName))
|
|
55
|
+
return;
|
|
56
|
+
const binding = path.scope.getBinding(currentName);
|
|
57
|
+
if (!binding)
|
|
58
|
+
return;
|
|
59
|
+
let sawUrlDestructure = false;
|
|
60
|
+
let propertyAccesses = 0;
|
|
61
|
+
for (const referencePath of binding.referencePaths) {
|
|
62
|
+
const hasDestructure = hasUrlDestructure(referencePath);
|
|
63
|
+
if (hasDestructure) {
|
|
64
|
+
sawUrlDestructure = true;
|
|
65
|
+
}
|
|
66
|
+
if (propertyAccesses < 2 && isUrlPropertyAccess(referencePath)) {
|
|
67
|
+
propertyAccesses += 1;
|
|
68
|
+
}
|
|
69
|
+
if (sawUrlDestructure || propertyAccesses >= 2)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (!sawUrlDestructure && propertyAccesses < 2)
|
|
73
|
+
return;
|
|
74
|
+
const candidateName = "url";
|
|
75
|
+
if (candidateName === currentName)
|
|
76
|
+
return;
|
|
77
|
+
const programScope = path.scope.getProgramParent();
|
|
78
|
+
if (Object.hasOwn(programScope.globals, candidateName))
|
|
79
|
+
return;
|
|
80
|
+
group.add({
|
|
81
|
+
scope: path.scope,
|
|
82
|
+
currentName,
|
|
83
|
+
baseName: candidateName,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
transformationsApplied += group.apply();
|
|
88
|
+
}
|
|
89
|
+
return Promise.resolve({
|
|
90
|
+
nodesVisited,
|
|
91
|
+
transformationsApplied,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const urlPropertyNames = new Set([
|
|
2
|
+
"hash",
|
|
3
|
+
"host",
|
|
4
|
+
"hostname",
|
|
5
|
+
"href",
|
|
6
|
+
"origin",
|
|
7
|
+
"password",
|
|
8
|
+
"pathname",
|
|
9
|
+
"port",
|
|
10
|
+
"protocol",
|
|
11
|
+
"search",
|
|
12
|
+
"searchParams",
|
|
13
|
+
"username",
|
|
14
|
+
]);
|
|
15
|
+
const countUrlProperties = (pattern) => {
|
|
16
|
+
let count = 0;
|
|
17
|
+
for (const property of pattern.properties) {
|
|
18
|
+
if (property.type !== "ObjectProperty")
|
|
19
|
+
continue;
|
|
20
|
+
if (property.computed)
|
|
21
|
+
continue;
|
|
22
|
+
const key = property.key;
|
|
23
|
+
const keyName = key.type === "Identifier"
|
|
24
|
+
? key.name
|
|
25
|
+
: key.type === "StringLiteral"
|
|
26
|
+
? key.value
|
|
27
|
+
: undefined;
|
|
28
|
+
if (keyName && urlPropertyNames.has(keyName)) {
|
|
29
|
+
count += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return count;
|
|
33
|
+
};
|
|
34
|
+
export const hasUrlDestructure = (referencePath) => {
|
|
35
|
+
const parent = referencePath.parentPath;
|
|
36
|
+
if (!parent)
|
|
37
|
+
return false;
|
|
38
|
+
if (parent.isVariableDeclarator() &&
|
|
39
|
+
parent.node.init === referencePath.node &&
|
|
40
|
+
parent.node.id.type === "ObjectPattern") {
|
|
41
|
+
return countUrlProperties(parent.node.id) >= 2;
|
|
42
|
+
}
|
|
43
|
+
if (parent.isAssignmentExpression() &&
|
|
44
|
+
parent.node.operator === "=" &&
|
|
45
|
+
parent.node.right === referencePath.node &&
|
|
46
|
+
parent.node.left.type === "ObjectPattern") {
|
|
47
|
+
return countUrlProperties(parent.node.left) >= 2;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "miniread",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.38.0",
|
|
6
6
|
"description": "Transform minified JavaScript/TypeScript into a more readable form using deterministic AST-based transforms.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|