@vernikr/size-report 2.3.0 → 2.5.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/README.md +929 -1051
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +33 -35
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +81 -69
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +78 -87
- package/src/page/app.js +31 -30
- package/src/page/build.js +40 -39
- package/src/page/dom.js +16 -3
- package/src/page/panel.js +85 -64
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -639
package/src/strip/js.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/*
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
1
|
+
/* Comments and indentation stripped — "volume without ballast" rather than minification:
|
|
2
|
+
* spaces inside strings and the order of tokens stay untouched, which keeps numbers
|
|
3
|
+
* comparable across languages and keeps this path free of any foreign tool (the real
|
|
4
|
+
* minifier is an optional dependency, and this is what remains without it). Strings and
|
|
5
|
+
* templates pass through whole, a block comment becomes a space — so that `a` and `b` around
|
|
6
|
+
* one do not merge into a single name — and the newline after `//` survives, because it
|
|
7
|
+
* separates tokens.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* The pass is split by case: comment, regex, string, ordinary character. A case reports how
|
|
10
|
+
* much it consumed (`false` — not its turn), and all of them share one pass state; a state
|
|
11
|
+
* per case is not kept because the decision about a regex depends on everything printed so
|
|
12
|
+
* far.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
/* Pass state: the text, the position in it, and what tells a regex from a division — the last
|
|
16
|
+
* significant character printed and the tail of the last word (`return` is followed by an
|
|
17
|
+
* expression rather than by a division). */
|
|
18
18
|
function scanOf(src) {
|
|
19
19
|
return { src: src, out: '', i: 0, last: '', word: '' };
|
|
20
20
|
}
|
|
@@ -22,15 +22,15 @@ function scanOf(src) {
|
|
|
22
22
|
export function stripJs(src) {
|
|
23
23
|
const s = scanOf(src);
|
|
24
24
|
while (s.i < src.length) {
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
25
|
+
/* Comment pairs are checked before regexes: neither `/` nor `*` can start a regex
|
|
26
|
+
* literal, while `/*` is an ordinary thing to meet. */
|
|
27
27
|
if (skipLineComment(s) || skipBlockComment(s) || takeRegex(s) || takeString(s)) continue;
|
|
28
28
|
putChar(s);
|
|
29
29
|
}
|
|
30
30
|
return s.out;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// A line comment: `//` is eaten up to the newline, the newline itself stays.
|
|
34
34
|
function skipLineComment(s) {
|
|
35
35
|
if (s.src[s.i] !== '/' || s.src[s.i + 1] !== '/') return false;
|
|
36
36
|
const nl = s.src.indexOf('\n', s.i);
|
|
@@ -38,7 +38,7 @@ function skipLineComment(s) {
|
|
|
38
38
|
return true;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// A block comment becomes a space: it separates names without taking up volume.
|
|
42
42
|
function skipBlockComment(s) {
|
|
43
43
|
if (s.src[s.i] !== '/' || s.src[s.i + 1] !== '*') return false;
|
|
44
44
|
const end = s.src.indexOf('*/', s.i + 2);
|
|
@@ -47,10 +47,10 @@ function skipBlockComment(s) {
|
|
|
47
47
|
return true;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
/* A regex starts where an operand is expected: after an operator, an opening bracket or a
|
|
51
|
+
* keyword. The sign is crude, and it can afford to be: a regex this pass mistakes for code,
|
|
52
|
+
* or code it mistakes for a comment, does not compile — and that is exactly what the guard
|
|
53
|
+
* checks (`strip/guard.js`). */
|
|
54
54
|
const REGEX_KEYWORDS = ['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'case', 'do', 'else', 'yield', 'await'];
|
|
55
55
|
|
|
56
56
|
function regexAllowed(last, word) {
|
|
@@ -75,7 +75,7 @@ function endOfRegex(src, start) {
|
|
|
75
75
|
while (i < src.length) {
|
|
76
76
|
const ch = src[i];
|
|
77
77
|
if (ch === '\\') { i += 2; continue; }
|
|
78
|
-
if (ch === '\n') return start + 1; //
|
|
78
|
+
if (ch === '\n') return start + 1; // a newline means this was not a regex after all
|
|
79
79
|
if (ch === '[') inClass = true;
|
|
80
80
|
else if (ch === ']') inClass = false;
|
|
81
81
|
else if (ch === '/' && !inClass) return i + 1;
|
|
@@ -84,7 +84,7 @@ function endOfRegex(src, start) {
|
|
|
84
84
|
return start + 1;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// A string or a template passes through whole, its content included.
|
|
88
88
|
function takeString(s) {
|
|
89
89
|
const quote = s.src[s.i];
|
|
90
90
|
if (quote !== '"' && quote !== "'" && quote !== '`') return false;
|
|
@@ -102,7 +102,7 @@ function endOfString(src, start, quote) {
|
|
|
102
102
|
const ch = src[i];
|
|
103
103
|
if (ch === '\\') { i += 2; continue; }
|
|
104
104
|
if (ch === quote) return i + 1;
|
|
105
|
-
//
|
|
105
|
+
// A template holds an expression in `${…}`, and that expression has strings of its own.
|
|
106
106
|
if (quote === '`' && ch === '$' && src[i + 1] === '{') { i = endOfTemplateExpr(src, i + 2); continue; }
|
|
107
107
|
i++;
|
|
108
108
|
}
|
|
@@ -123,8 +123,8 @@ function endOfTemplateExpr(src, start) {
|
|
|
123
123
|
return src.length;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/*
|
|
127
|
-
*
|
|
126
|
+
/* An ordinary character: it goes into the output, and the pass state remembers by it where a
|
|
127
|
+
* regex may stand. */
|
|
128
128
|
function putChar(s) {
|
|
129
129
|
const ch = s.src[s.i];
|
|
130
130
|
s.out += ch;
|
package/src/strip.js
CHANGED
|
@@ -2,16 +2,13 @@ import path from 'path';
|
|
|
2
2
|
import { stripJs } from './strip/js.js';
|
|
3
3
|
import { compactJson, stripCss, stripHtml, stripLines } from './strip/forms.js';
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
5
|
+
/* Stripping ballast: which form of text applies to which file. Only text transformation —
|
|
6
|
+
* the module knows no history and reads no settings, only the strategy it is handed. What
|
|
7
|
+
* binds the forms to a file lives here: the extension, the strategy, and which strategies
|
|
8
|
+
* count as exact.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (`strip/guard.js`). Здесь остаётся то, что связывает их с файлом: расширение,
|
|
11
|
-
* стратегия и что считать точным числом.
|
|
12
|
-
*
|
|
13
|
-
* Имена форм наружу отдаются отсюда же: точка входа пакета берёт их по одному
|
|
14
|
-
* адресу, и переезд разбора не должен быть виден тому, кто на них опирался. */
|
|
10
|
+
* The forms are re-exported from here, so that the package entry point has one address for
|
|
11
|
+
* them and a move inside the parsing stays invisible to whoever relied on them. */
|
|
15
12
|
|
|
16
13
|
export { stripJs, stripCss, stripHtml, stripLines, compactJson };
|
|
17
14
|
export { assertCompilable } from './strip/guard.js';
|
|
@@ -20,10 +17,10 @@ export function byteLen(text) {
|
|
|
20
17
|
return Buffer.byteLength(text, 'utf8');
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
/*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
/* The strategy for an extension. An unknown extension gets indentation and blank lines
|
|
21
|
+
* removed — the safe minimum: stripping comments by eye in a syntax the generator was never
|
|
22
|
+
* taught (`#` in YAML, indentation in Python) would measure a different file than the one on
|
|
23
|
+
* disk. */
|
|
27
24
|
const MINIFY_BY_EXT = {
|
|
28
25
|
'.js': 'strip-js', '.mjs': 'strip-js', '.cjs': 'strip-js',
|
|
29
26
|
'.html': 'strip-html', '.htm': 'strip-html',
|
|
@@ -32,11 +29,11 @@ const MINIFY_BY_EXT = {
|
|
|
32
29
|
};
|
|
33
30
|
export const STRATEGIES = ['strip-js', 'strip-html', 'strip-css', 'json', 'strip-lines', 'none'];
|
|
34
31
|
|
|
35
|
-
/*
|
|
36
|
-
* (
|
|
37
|
-
* —
|
|
38
|
-
*
|
|
39
|
-
*
|
|
32
|
+
/* The strategies that are minification itself: JSON loses only insignificant whitespace
|
|
33
|
+
* (numbers take their shortest form) and nobody can make it shorter. The rest are a
|
|
34
|
+
* simplification — they drop ballast but neither rename nor restructure code, so no exact
|
|
35
|
+
* number can be promised for them. The list is owned here, next to the strategies, and the
|
|
36
|
+
* metric reads it to decide whether its number is exact or approximate. */
|
|
40
37
|
export const EXACT_STRATEGIES = ['json'];
|
|
41
38
|
|
|
42
39
|
export function strategyFor(file, cfg) {
|
|
@@ -44,8 +41,8 @@ export function strategyFor(file, cfg) {
|
|
|
44
41
|
return (cfg.minify.ext && cfg.minify.ext[ext]) || MINIFY_BY_EXT[ext] || 'strip-lines';
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
/*
|
|
48
|
-
*
|
|
44
|
+
/* What a strategy does belongs to the strategy rather than to this list: the dispatcher only
|
|
45
|
+
* picks whom to call, and repeats the strategy names because the parsing lives elsewhere. */
|
|
49
46
|
export function minifyForm(text, file, cfg) {
|
|
50
47
|
const how = strategyFor(file, cfg);
|
|
51
48
|
if (how === 'none') return text;
|
|
@@ -54,5 +51,5 @@ export function minifyForm(text, file, cfg) {
|
|
|
54
51
|
if (how === 'strip-css') return stripLines(stripCss(text));
|
|
55
52
|
if (how === 'json') return compactJson(text);
|
|
56
53
|
if (how === 'strip-lines') return stripLines(text);
|
|
57
|
-
throw new Error('
|
|
54
|
+
throw new Error('unknown minification strategy "' + how + '" (have: ' + STRATEGIES.join(', ') + ')');
|
|
58
55
|
}
|
package/src/table.css
CHANGED
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
/*
|
|
2
|
-
*
|
|
1
|
+
/* One type face for the whole table; numbers line up by their digits thanks to tabular-nums rather than through a
|
|
2
|
+
* monospaced font. */
|
|
3
3
|
table { border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
4
4
|
th, td { padding: 2px 7px; border-bottom: 1px solid rgba(127, 127, 127, .25); white-space: nowrap; }
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
5
|
+
/* A two-row header: both rows stick, so the second is offset by exactly the first one's height (line-height 20 plus the
|
|
6
|
+
* 2px bottom border), or the rows would overlap. */
|
|
7
7
|
thead th { position: sticky; top: 0; z-index: 3; background: Canvas; text-align: center; line-height: 20px; padding: 0 7px; }
|
|
8
8
|
thead tr:first-child th { border-bottom-width: 2px; }
|
|
9
9
|
thead tr:last-child th { top: 22px; }
|
|
10
10
|
.num { text-align: right; }
|
|
11
11
|
.g { border-left: 1px solid rgba(127, 127, 127, .35); }
|
|
12
|
-
/*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
/* The sticky left column: an opaque background (Canvas), or numbers would show through the cell when scrolling sideways. A
|
|
13
|
+
* tier above the neighbouring cells (2) and below the header (3); the header's corner is above them all, or the column groups
|
|
14
|
+
* would crawl over the commit column. */
|
|
15
15
|
.c-commit { position: sticky; left: 0; z-index: 2; background: Canvas; text-align: left; font-weight: 400; }
|
|
16
16
|
.c-commit a { color: inherit; }
|
|
17
17
|
thead .c-commit { z-index: 6; }
|
|
18
|
-
/*
|
|
19
|
-
*
|
|
18
|
+
/* This block sets the column's width. Without it the cell's content went past its borders and was drawn over the neighbouring
|
|
19
|
+
* numbers: a table cell clips nothing. */
|
|
20
20
|
.clip { display: flex; align-items: baseline; gap: 6px; width: 300px; }
|
|
21
21
|
.when { flex: none; opacity: .7; }
|
|
22
22
|
.subj { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
23
23
|
.subj.plain { opacity: .7; }
|
|
24
24
|
.sect { flex: none; opacity: .7; font-size: 11px; text-decoration: none; border-bottom: 1px dotted currentColor; }
|
|
25
|
-
/*
|
|
26
|
-
* логики», а не тревога). */
|
|
25
|
+
/* Growth green, fall red — by agreement with whoever asked for the report (growth is "more logic" rather than alarm). */
|
|
27
26
|
.up { color: #1e8449; }
|
|
28
27
|
.down { color: #c0392b; }
|
|
29
28
|
.miss { opacity: .5; }
|
|
30
|
-
/*
|
|
29
|
+
/* The top row holds the current sizes: it is also what explains what the deltas refer to. */
|
|
31
30
|
tr.now th, tr.now td { border-bottom: 2px solid rgba(127, 127, 127, .35); }
|
|
32
31
|
tr.now .c-commit { font-weight: 600; }
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
32
|
+
/* A row's highlight is laid over rather than swapped in: the sticky column has to stay opaque, or numbers show through it
|
|
33
|
+
* when scrolling. */
|
|
35
34
|
tbody tr:hover th, tbody tr:hover td { background-image: linear-gradient(rgba(127, 127, 127, .08), rgba(127, 127, 127, .08)); }
|
package/src/tokens.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { loadOptional } from './optional.js';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
/* The tokenizer follows the same discipline as the minifier: an optional dependency, loaded
|
|
5
|
+
* lazily (how that works: `src/optional.js`). One difference: the tokenizer takes any text and
|
|
6
|
+
* has nothing to refuse, so a missing dependency is not a refusal but a different count — an
|
|
7
|
+
* estimate by length, marked as an approximation in the metric label.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The family is about models, the encoding about the number: the same file counts differently
|
|
10
|
+
* under `cl100k_base` and `o200k_base`, which is why the encoding is chosen next to the family
|
|
11
|
+
* instead of being implied. There is one family here, and that is not an omission: the others
|
|
12
|
+
* have no dictionary that could be called their own, and counting with someone else's while
|
|
13
|
+
* calling it a family would promise what does not exist. */
|
|
14
14
|
|
|
15
15
|
export const TOKEN_FAMILIES = {
|
|
16
16
|
openai: { tool: 'gpt-tokenizer', encodings: ['o200k_base', 'cl100k_base'] }
|
|
@@ -18,24 +18,25 @@ export const TOKEN_FAMILIES = {
|
|
|
18
18
|
|
|
19
19
|
export const TOKEN_DEFAULTS = { family: 'openai', encoding: 'o200k_base' };
|
|
20
20
|
|
|
21
|
-
/*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
/* The estimate without a dictionary. The coefficient was taken from this repository's own
|
|
22
|
+
* texts (Russian documents and code): `README.md` gave 3.1 characters per token, the archived
|
|
23
|
+
* journal `worklog/archive/WORKLOG.md` about 3.0. For Latin script the same estimate overstates
|
|
24
|
+
* the count (about 4 characters per token there), which is why it is marked as an
|
|
25
|
+
* approximation. */
|
|
25
26
|
export const CHARS_PER_TOKEN = 3;
|
|
26
27
|
|
|
27
|
-
/*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
/* Formats for which counting tokens makes no sense: a picture, a font or an archive is bytes,
|
|
29
|
+
* and the tokenizer would split them into anything at all, giving a random number. The list
|
|
30
|
+
* exists so that the metric says this in words instead of passing such a count off as counted.
|
|
31
|
+
* SVG is deliberately not here: it is text, and its tokens are meaningful. */
|
|
31
32
|
export const BINARY_EXTS = [
|
|
32
33
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.avif',
|
|
33
34
|
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
34
35
|
'.pdf', '.zip', '.gz', '.tar', '.mp4', '.mp3', '.mov'
|
|
35
36
|
];
|
|
36
37
|
|
|
37
|
-
/*
|
|
38
|
-
*
|
|
38
|
+
/* A dictionary is loaded once per encoding: megabytes of tables stand behind it, and paying
|
|
39
|
+
* for them on every file would have no justification. */
|
|
39
40
|
const probed = new Map();
|
|
40
41
|
|
|
41
42
|
export function tokenizer(settings) {
|
|
@@ -51,26 +52,26 @@ function familyOf(settings) {
|
|
|
51
52
|
return TOKEN_FAMILIES[asked] === undefined ? TOKEN_DEFAULTS.family : asked;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
/*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
/* Counting one text: with the dictionary if there is one, by estimate otherwise. Both answers
|
|
56
|
+
* are a number of text units, and what tells them apart is the metric label (`accuracy`) rather
|
|
57
|
+
* than the value, which is why one cannot be passed off as the other. */
|
|
57
58
|
export function tokenCount(text, settings) {
|
|
58
59
|
const { tool } = tokenizer(settings);
|
|
59
60
|
if (tool === null) return estimate(text);
|
|
60
61
|
return tool.encode(text).length;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
/*
|
|
64
|
-
*
|
|
64
|
+
/* An estimate by length is all that can be said without a dictionary. Characters are counted
|
|
65
|
+
* as code points: for non-ASCII that is closer to the number of tokens than UTF-16 units. */
|
|
65
66
|
export function estimate(text) {
|
|
66
67
|
let chars = 0;
|
|
67
68
|
for (const _ch of text) chars++;
|
|
68
69
|
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
/* Whether the file is binary, since counting tokens means nothing for it. The list of formats
|
|
73
|
+
* is owned here, so both the metric label and the cell mark ask about a file here instead of
|
|
74
|
+
* keeping a list of their own. */
|
|
74
75
|
export function isBinary(file) {
|
|
75
76
|
return BINARY_EXTS.indexOf(path.extname(file).toLowerCase()) >= 0;
|
|
76
77
|
}
|
package/src/tool.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* отсутствие манифеста не повод не собирать таблицу. */
|
|
3
|
+
/* The package's own metadata: the name and the version are read from its manifest so that no second copy exists. A module
|
|
4
|
+
* of its own because these data describe the packaging rather than a consumer project, and the contract of the data needs
|
|
5
|
+
* them.
|
|
6
|
+
*
|
|
7
|
+
* With no manifest (someone else's build) the placeholder stays: the version is needed in the data alone, and a missing
|
|
8
|
+
* manifest is no reason not to build a report. */
|
|
10
9
|
export let TOOL_PKG = { name: '@vernikr/size-report', version: '0.0.0' };
|
|
11
10
|
try {
|
|
12
11
|
TOOL_PKG = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
12
|
} catch (_e) {}
|
|
14
13
|
|
|
15
|
-
/*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
/* How the package is installed into a project — the git link to the release, the pinned form `README.md` teaches: the
|
|
15
|
+
* advice has to name the revision the documentation describes rather than whatever the registry serves as the latest one.
|
|
16
|
+
* The address and the version come from the manifest, so the advice cannot drift from the release, and with no address
|
|
17
|
+
* (`null`) there is nothing to name. */
|
|
19
18
|
export function installSpec() {
|
|
20
19
|
const repo = TOOL_PKG.repository === undefined ? ''
|
|
21
20
|
: (typeof TOOL_PKG.repository === 'string' ? TOOL_PKG.repository : TOOL_PKG.repository.url || '');
|