From 151c70dd1749284e580e95e18a9fc3f4081c0a65 Mon Sep 17 00:00:00 2001 From: chris48s Date: Sun, 1 Dec 2024 19:53:26 +0000 Subject: [PATCH] Add ability to format bytes as metric or IEC; affects [bundlejs bundlephobia ChromeWebStoreSize CratesSize DockerSize GithubRepoSize GithubCodeSize GithubSize NpmUnpackedSize SpigetDownloadSize steam VisualStudioAppCenterReleasesSize whatpulse] (#10547) * add renderSizeBadge helper, use it everywhere - switch from pretty-bytes to byte-size - add renderSizeBadge() helper function - match upstream conventions for metric/IEC units - add new test helpers and use them in service tests * unrelated: fix npm unpacked size query param schema not strictly related to this PR but I noticed it was broken * chromewebstore: reformat size string, test against isIecFileSize --- package-lock.json | 21 +++++++--------- package.json | 2 +- services/bundlejs/bundlejs-package.service.js | 15 ++++------- services/bundlejs/bundlejs-package.tester.js | 12 ++++----- services/bundlephobia/bundlephobia.service.js | 7 ++---- services/bundlephobia/bundlephobia.tester.js | 18 ++++++------- .../chrome-web-store-size.service.js | 15 +++++++++-- .../chrome-web-store-size.tester.js | 4 +-- services/crates/crates-size.service.js | 12 ++------- services/crates/crates-size.tester.js | 6 ++--- services/docker/docker-size.service.js | 8 ++---- services/docker/docker-size.tester.js | 12 ++++----- services/github/github-code-size.service.js | 11 ++------ services/github/github-code-size.tester.js | 4 +-- services/github/github-repo-size.service.js | 14 +++-------- services/github/github-repo-size.tester.js | 4 +-- services/github/github-size.service.js | 11 ++------ services/github/github-size.tester.js | 10 ++++---- services/npm/npm-unpacked-size.service.js | 15 ++++++++--- services/npm/npm-unpacked-size.tester.js | 10 ++++---- services/size.js | 25 +++++++++++++++++++ .../spiget/spiget-download-size.tester.js | 4 +-- services/steam/steam-workshop.service.js | 8 ++---- services/steam/steam-workshop.tester.js | 8 ++++-- services/test-validators.js | 8 ++++-- ...studio-app-center-releases-size.service.js | 10 ++------ ...-studio-app-center-releases-size.tester.js | 6 ++--- services/whatpulse/whatpulse.tester.js | 6 ++--- 28 files changed, 142 insertions(+), 144 deletions(-) create mode 100644 services/size.js diff --git a/package-lock.json b/package-lock.json index a516bd7f77379..b282c3e218a85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@shields_io/camp": "^18.1.2", "@xmldom/xmldom": "0.9.5", "badge-maker": "file:badge-maker", + "byte-size": "^9.0.0", "bytes": "^3.1.2", "camelcase": "^8.0.0", "chalk": "^5.3.0", @@ -45,7 +46,6 @@ "parse-link-header": "^2.0.0", "path-to-regexp": "^6.3.0", "pg": "^8.13.1", - "pretty-bytes": "^6.1.1", "priorityqueuejs": "^2.0.0", "prom-client": "^15.1.3", "qs": "^6.13.1", @@ -8132,6 +8132,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byte-size": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.0.tgz", + "integrity": "sha512-xrJ8Hki7eQ6xew55mM6TG9zHI852OoAHcPfduWWtR6yxk2upTuIZy13VioRBDyHReHDdbeDPifUboeNkK/sXXA==", + "engines": { + "node": ">=12.17" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -24578,17 +24586,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", diff --git a/package.json b/package.json index a7a687041e1b4..775eade594faa 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@shields_io/camp": "^18.1.2", "@xmldom/xmldom": "0.9.5", "badge-maker": "file:badge-maker", + "byte-size": "^9.0.0", "bytes": "^3.1.2", "camelcase": "^8.0.0", "chalk": "^5.3.0", @@ -57,7 +58,6 @@ "parse-link-header": "^2.0.0", "path-to-regexp": "^6.3.0", "pg": "^8.13.1", - "pretty-bytes": "^6.1.1", "priorityqueuejs": "^2.0.0", "prom-client": "^15.1.3", "qs": "^6.13.1", diff --git a/services/bundlejs/bundlejs-package.service.js b/services/bundlejs/bundlejs-package.service.js index 27b7021b000af..5624f00868ee8 100644 --- a/services/bundlejs/bundlejs-package.service.js +++ b/services/bundlejs/bundlejs-package.service.js @@ -1,9 +1,11 @@ import Joi from 'joi' import { BaseJsonService, pathParam, queryParam } from '../index.js' +import { renderSizeBadge } from '../size.js' +import { nonNegativeInteger } from '../validators.js' const schema = Joi.object({ size: Joi.object({ - compressedSize: Joi.string().required(), + rawCompressedSize: nonNegativeInteger, }).required(), }).required() @@ -76,13 +78,6 @@ export default class BundlejsPackage extends BaseJsonService { static defaultBadgeData = { label: 'bundlejs', color: 'informational' } - static render({ size }) { - return { - label: 'minified size (gzip)', - message: size, - } - } - async fetch({ scope, packageName, exports }) { const searchParams = { q: `${scope ? `${scope}/` : ''}${packageName}`, @@ -110,7 +105,7 @@ export default class BundlejsPackage extends BaseJsonService { async handle({ scope, packageName }, { exports }) { const json = await this.fetch({ scope, packageName, exports }) - const size = json.size.compressedSize - return this.constructor.render({ size }) + const size = json.size.rawCompressedSize + return renderSizeBadge(size, 'metric', 'minified size (gzip)') } } diff --git a/services/bundlejs/bundlejs-package.tester.js b/services/bundlejs/bundlejs-package.tester.js index 1509826f3dbef..4522fb81b5b8c 100644 --- a/services/bundlejs/bundlejs-package.tester.js +++ b/services/bundlejs/bundlejs-package.tester.js @@ -1,26 +1,26 @@ -import { isFileSize } from '../test-validators.js' +import { isMetricFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('bundlejs/package (packageName)') .get('/jquery.json') - .expectBadge({ label: 'minified size (gzip)', message: isFileSize }) + .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize }) t.create('bundlejs/package (version)') .get('/react@18.2.0.json') - .expectBadge({ label: 'minified size (gzip)', message: isFileSize }) + .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize }) t.create('bundlejs/package (scoped)') .get('/@cycle/rx-run.json') - .expectBadge({ label: 'minified size (gzip)', message: isFileSize }) + .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize }) t.create('bundlejs/package (select exports)') .get('/value-enhancer.json?exports=isVal,val') - .expectBadge({ label: 'minified size (gzip)', message: isFileSize }) + .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize }) t.create('bundlejs/package (scoped version select exports)') .get('/@ngneat/falso@6.4.0.json?exports=randEmail,randFullName') - .expectBadge({ label: 'minified size (gzip)', message: isFileSize }) + .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize }) t.create('bundlejs/package (not found)') .get('/react@18.2.0.json') diff --git a/services/bundlephobia/bundlephobia.service.js b/services/bundlephobia/bundlephobia.service.js index 0b182d59f8ded..11be8f9294a1a 100644 --- a/services/bundlephobia/bundlephobia.service.js +++ b/services/bundlephobia/bundlephobia.service.js @@ -1,5 +1,5 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' +import { renderSizeBadge } from '../size.js' import { nonNegativeInteger } from '../validators.js' import { BaseJsonService, pathParams } from '../index.js' @@ -112,10 +112,7 @@ export default class Bundlephobia extends BaseJsonService { static render({ format, size }) { const label = format === 'min' ? 'minified size' : 'minzipped size' - return { - label, - message: prettyBytes(size), - } + return renderSizeBadge(size, 'iec', label) } async fetch({ scope, packageName, version }) { diff --git a/services/bundlephobia/bundlephobia.tester.js b/services/bundlephobia/bundlephobia.tester.js index 525bff0e026b3..3bd229094b0be 100644 --- a/services/bundlephobia/bundlephobia.tester.js +++ b/services/bundlephobia/bundlephobia.tester.js @@ -1,4 +1,4 @@ -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() @@ -13,42 +13,42 @@ const data = [ { format: formats.A, get: '/min/preact.json', - expect: { label: 'minified size', message: isFileSize }, + expect: { label: 'minified size', message: isIecFileSize }, }, { format: formats.B, get: '/min/preact/8.0.0.json', - expect: { label: 'minified size', message: isFileSize }, + expect: { label: 'minified size', message: isIecFileSize }, }, { format: formats.C, get: '/min/@cycle/core.json', - expect: { label: 'minified size', message: isFileSize }, + expect: { label: 'minified size', message: isIecFileSize }, }, { format: formats.D, get: '/min/@cycle/core/7.0.0.json', - expect: { label: 'minified size', message: isFileSize }, + expect: { label: 'minified size', message: isIecFileSize }, }, { format: formats.A, get: '/minzip/preact.json', - expect: { label: 'minzipped size', message: isFileSize }, + expect: { label: 'minzipped size', message: isIecFileSize }, }, { format: formats.B, get: '/minzip/preact/8.0.0.json', - expect: { label: 'minzipped size', message: isFileSize }, + expect: { label: 'minzipped size', message: isIecFileSize }, }, { format: formats.C, get: '/minzip/@cycle/core.json', - expect: { label: 'minzipped size', message: isFileSize }, + expect: { label: 'minzipped size', message: isIecFileSize }, }, { format: formats.D, get: '/minzip/@cycle/core/7.0.0.json', - expect: { label: 'minzipped size', message: isFileSize }, + expect: { label: 'minzipped size', message: isIecFileSize }, }, { format: formats.A, diff --git a/services/chrome-web-store/chrome-web-store-size.service.js b/services/chrome-web-store/chrome-web-store-size.service.js index 12b11829e454a..040fc9a1a6d53 100644 --- a/services/chrome-web-store/chrome-web-store-size.service.js +++ b/services/chrome-web-store/chrome-web-store-size.service.js @@ -1,4 +1,4 @@ -import { NotFound, pathParams } from '../index.js' +import { InvalidResponse, NotFound, pathParams } from '../index.js' import BaseChromeWebStoreService from './chrome-web-store-base.js' export default class ChromeWebStoreSize extends BaseChromeWebStoreService { @@ -22,6 +22,17 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService { color: 'blue', } + transform(sizeStr) { + const match = sizeStr.match(/^(\d+)([a-zA-Z]+)$/) + if (!match) { + throw new InvalidResponse({ + prettyMessage: 'size does not match expected format', + }) + } + const [, size, units] = match + return `${size} ${units}` + } + async handle({ storeId }) { const chromeWebStore = await this.fetch({ storeId }) const size = chromeWebStore.size() @@ -30,6 +41,6 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService { throw new NotFound({ prettyMessage: 'not found' }) } - return { message: size } + return { message: this.transform(size) } } } diff --git a/services/chrome-web-store/chrome-web-store-size.tester.js b/services/chrome-web-store/chrome-web-store-size.tester.js index 5eb2f9ae6d4cd..40e8d62661903 100644 --- a/services/chrome-web-store/chrome-web-store-size.tester.js +++ b/services/chrome-web-store/chrome-web-store-size.tester.js @@ -1,11 +1,11 @@ import { createServiceTester } from '../tester.js' +import { isIecFileSize } from '../test-validators.js' export const t = await createServiceTester() -const isFileSize = /^\d+(\.\d+)?(MiB|KiB)$/ t.create('Size').get('/nccfelhkfpbnefflolffkclhenplhiab.json').expectBadge({ label: 'extension size', - message: isFileSize, + message: isIecFileSize, }) t.create('Size (not found)') diff --git a/services/crates/crates-size.service.js b/services/crates/crates-size.service.js index d5a58292e8c7d..bc3b705e3c7f1 100644 --- a/services/crates/crates-size.service.js +++ b/services/crates/crates-size.service.js @@ -1,5 +1,5 @@ -import prettyBytes from 'pretty-bytes' import { pathParams } from '../index.js' +import { renderSizeBadge } from '../size.js' import { BaseCratesService, description } from './crates-base.js' export default class CratesSize extends BaseCratesService { @@ -38,17 +38,9 @@ export default class CratesSize extends BaseCratesService { }, } - render({ size }) { - return { - label: 'size', - message: prettyBytes(size), - color: 'blue', - } - } - async handle({ crate, version }) { const json = await this.fetch({ crate, version }) const size = this.constructor.getVersionObj(json).crate_size - return this.render({ size }) + return renderSizeBadge(size, 'iec') } } diff --git a/services/crates/crates-size.tester.js b/services/crates/crates-size.tester.js index 710ab99a35963..7512aa62b784e 100644 --- a/services/crates/crates-size.tester.js +++ b/services/crates/crates-size.tester.js @@ -1,14 +1,14 @@ import { createServiceTester } from '../tester.js' -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' export const t = await createServiceTester() t.create('size') .get('/tokio.json') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isIecFileSize }) t.create('size (with version)') .get('/tokio/1.32.0.json') - .expectBadge({ label: 'size', message: '725 kB' }) + .expectBadge({ label: 'size', message: '708 KiB' }) t.create('size (not found)') .get('/not-a-crate.json') diff --git a/services/docker/docker-size.service.js b/services/docker/docker-size.service.js index 80cb74f6e673b..99db5bd786253 100644 --- a/services/docker/docker-size.service.js +++ b/services/docker/docker-size.service.js @@ -1,5 +1,5 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' +import { renderSizeBadge } from '../size.js' import { nonNegativeInteger } from '../validators.js' import { latest } from '../version.js' import { BaseJsonService, NotFound, pathParams, queryParams } from '../index.js' @@ -124,10 +124,6 @@ export default class DockerSize extends BaseJsonService { static defaultBadgeData = { label: 'image size', color: 'blue' } - static render({ size }) { - return { message: prettyBytes(size) } - } - async fetch({ user, repo, tag, page }) { page = page ? `&page=${page}` : '' return await fetch(this, { @@ -233,6 +229,6 @@ export default class DockerSize extends BaseJsonService { } const { size } = await this.transform({ tag, sort, data, arch }) - return this.constructor.render({ size }) + return renderSizeBadge(size, 'iec', 'image size') } } diff --git a/services/docker/docker-size.tester.js b/services/docker/docker-size.tester.js index f6b9014bfab80..a5f078946e13c 100644 --- a/services/docker/docker-size.tester.js +++ b/services/docker/docker-size.tester.js @@ -1,4 +1,4 @@ -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() @@ -6,35 +6,35 @@ t.create('docker image size (valid, library)') .get('/_/alpine.json') .expectBadge({ label: 'image size', - message: isFileSize, + message: isIecFileSize, }) t.create('docker image size (valid, library, arch parameter )') .get('/_/mysql.json?arch=amd64') .expectBadge({ label: 'image size', - message: isFileSize, + message: isIecFileSize, }) t.create('docker image size (valid, library with tag)') .get('/_/alpine/latest.json') .expectBadge({ label: 'image size', - message: isFileSize, + message: isIecFileSize, }) t.create('docker image size (valid, user)') .get('/jrottenberg/ffmpeg.json') .expectBadge({ label: 'image size', - message: isFileSize, + message: isIecFileSize, }) t.create('docker image size (valid, user with tag)') .get('/jrottenberg/ffmpeg/3.2-alpine.json') .expectBadge({ label: 'image size', - message: isFileSize, + message: isIecFileSize, }) t.create('docker image size (invalid, incorrect tag)') diff --git a/services/github/github-code-size.service.js b/services/github/github-code-size.service.js index e0443238887ac..7ade29503099f 100644 --- a/services/github/github-code-size.service.js +++ b/services/github/github-code-size.service.js @@ -1,5 +1,5 @@ -import prettyBytes from 'pretty-bytes' import { pathParams } from '../index.js' +import { renderSizeBadge } from '../size.js' import { BaseGithubLanguage } from './github-languages-base.js' import { documentation } from './github-helpers.js' @@ -31,15 +31,8 @@ export default class GithubCodeSize extends BaseGithubLanguage { static defaultBadgeData = { label: 'code size' } - static render({ size }) { - return { - message: prettyBytes(size), - color: 'blue', - } - } - async handle({ user, repo }) { const data = await this.fetch({ user, repo }) - return this.constructor.render({ size: this.getTotalSize(data) }) + return renderSizeBadge(this.getTotalSize(data), 'iec', 'code size') } } diff --git a/services/github/github-code-size.tester.js b/services/github/github-code-size.tester.js index 39ec144e802f5..16c41129d798f 100644 --- a/services/github/github-code-size.tester.js +++ b/services/github/github-code-size.tester.js @@ -1,4 +1,4 @@ -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() @@ -6,7 +6,7 @@ t.create('code size in bytes for all languages') .get('/badges/shields.json') .expectBadge({ label: 'code size', - message: isFileSize, + message: isIecFileSize, }) t.create('code size in bytes for all languages (empty repo)') diff --git a/services/github/github-repo-size.service.js b/services/github/github-repo-size.service.js index be4c8df664a00..75fd71dc677d0 100644 --- a/services/github/github-repo-size.service.js +++ b/services/github/github-repo-size.service.js @@ -1,6 +1,6 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' import { pathParams } from '../index.js' +import { renderSizeBadge } from '../size.js' import { nonNegativeInteger } from '../validators.js' import { GithubAuthV3Service } from './github-auth-service.js' import { documentation, httpErrorsFor } from './github-helpers.js' @@ -33,14 +33,6 @@ export default class GithubRepoSize extends GithubAuthV3Service { static defaultBadgeData = { label: 'repo size' } - static render({ size }) { - return { - // note the GH API returns size in Kb - message: prettyBytes(size * 1024), - color: 'blue', - } - } - async fetch({ user, repo }) { return this._requestJson({ url: `/repos/${user}/${repo}`, @@ -51,6 +43,8 @@ export default class GithubRepoSize extends GithubAuthV3Service { async handle({ user, repo }) { const { size } = await this.fetch({ user, repo }) - return this.constructor.render({ size }) + // note the GH API returns size in KiB + // so we multiply by 1024 to get a size in bytes and then format that in IEC bytes + return renderSizeBadge(size * 1024, 'iec', 'repo size') } } diff --git a/services/github/github-repo-size.tester.js b/services/github/github-repo-size.tester.js index c39f5c700f4a2..7de73bc446dcd 100644 --- a/services/github/github-repo-size.tester.js +++ b/services/github/github-repo-size.tester.js @@ -1,10 +1,10 @@ -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('repository size').get('/badges/shields.json').expectBadge({ label: 'repo size', - message: isFileSize, + message: isIecFileSize, }) t.create('repository size (repo not found)') diff --git a/services/github/github-size.service.js b/services/github/github-size.service.js index 683cd6906adef..3b28e4b7d7ca9 100644 --- a/services/github/github-size.service.js +++ b/services/github/github-size.service.js @@ -1,5 +1,5 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' +import { renderSizeBadge } from '../size.js' import { nonNegativeInteger } from '../validators.js' import { NotFound, pathParam, queryParam } from '../index.js' import { GithubAuthV3Service } from './github-auth-service.js' @@ -44,13 +44,6 @@ export default class GithubSize extends GithubAuthV3Service { }, } - static render({ size }) { - return { - message: prettyBytes(size), - color: 'blue', - } - } - async fetch({ user, repo, path, branch }) { if (branch) { return this._requestJson({ @@ -73,6 +66,6 @@ export default class GithubSize extends GithubAuthV3Service { if (Array.isArray(body)) { throw new NotFound({ prettyMessage: 'not a regular file' }) } - return this.constructor.render({ size: body.size }) + return renderSizeBadge(body.size, 'iec') } } diff --git a/services/github/github-size.tester.js b/services/github/github-size.tester.js index 644b482dbafb4..5a425112e3cd7 100644 --- a/services/github/github-size.tester.js +++ b/services/github/github-size.tester.js @@ -1,10 +1,10 @@ -import { isFileSize } from '../test-validators.js' +import { isIecFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('File size') .get('/webcaetano/craft/build/phaser-craft.min.js.json') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isIecFileSize }) t.create('File size 404') .get('/webcaetano/craft/build/does-not-exist.min.js.json') @@ -20,12 +20,12 @@ t.create('File size for "not a regular file"') t.create('File size for a specified branch') .get('/webcaetano/craft/build/craft.min.js.json?branch=version-2') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isIecFileSize }) t.create('File size for a specified tag') .get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=2.1.2') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isIecFileSize }) t.create('File size for a specified commit') .get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=b848dbb') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isIecFileSize }) diff --git a/services/npm/npm-unpacked-size.service.js b/services/npm/npm-unpacked-size.service.js index 5dd7d64c54f1a..e187234db9220 100644 --- a/services/npm/npm-unpacked-size.service.js +++ b/services/npm/npm-unpacked-size.service.js @@ -1,8 +1,11 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' import { pathParam, queryParam } from '../index.js' +import { renderSizeBadge } from '../size.js' import { optionalNonNegativeInteger } from '../validators.js' -import NpmBase, { packageNameDescription } from './npm-base.js' +import NpmBase, { + packageNameDescription, + queryParamSchema, +} from './npm-base.js' const schema = Joi.object({ dist: Joi.object({ @@ -16,6 +19,7 @@ export default class NpmUnpackedSize extends NpmBase { static route = { base: 'npm/unpacked-size', pattern: ':scope(@[^/]+)?/:packageName/:version*', + queryParamSchema, } static openApi = { @@ -78,10 +82,13 @@ export default class NpmUnpackedSize extends NpmBase { }) const { unpackedSize } = dist + if (unpackedSize) { + return renderSizeBadge(unpackedSize, 'metric', 'unpacked size') + } return { label: 'unpacked size', - message: unpackedSize ? prettyBytes(unpackedSize) : 'unknown', - color: unpackedSize ? 'blue' : 'lightgray', + message: 'unknown', + color: 'lightgray', } } } diff --git a/services/npm/npm-unpacked-size.tester.js b/services/npm/npm-unpacked-size.tester.js index 7621b559f4cf2..436310d1aaba2 100644 --- a/services/npm/npm-unpacked-size.tester.js +++ b/services/npm/npm-unpacked-size.tester.js @@ -1,11 +1,11 @@ -import { isFileSize } from '../test-validators.js' +import { isMetricFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('Latest unpacked size') .get('/firereact.json') - .expectBadge({ label: 'unpacked size', message: isFileSize }) + .expectBadge({ label: 'unpacked size', message: isMetricFileSize }) t.create('Nonexistent unpacked size with version') .get('/express/4.16.0.json') @@ -13,15 +13,15 @@ t.create('Nonexistent unpacked size with version') t.create('Unpacked size with version') .get('/firereact/0.7.0.json') - .expectBadge({ label: 'unpacked size', message: '147 kB' }) + .expectBadge({ label: 'unpacked size', message: '147.2 kB' }) t.create('Unpacked size for scoped package') .get('/@testing-library/react.json') - .expectBadge({ label: 'unpacked size', message: isFileSize }) + .expectBadge({ label: 'unpacked size', message: isMetricFileSize }) t.create('Unpacked size for scoped package with version') .get('/@testing-library/react/14.2.1.json') - .expectBadge({ label: 'unpacked size', message: '5.41 MB' }) + .expectBadge({ label: 'unpacked size', message: '5.4 MB' }) t.create('Nonexistent unpacked size for scoped package with version') .get('/@cycle/rx-run/7.2.0.json') diff --git a/services/size.js b/services/size.js new file mode 100644 index 0000000000000..970e31e4a5b59 --- /dev/null +++ b/services/size.js @@ -0,0 +1,25 @@ +/** + * @module + */ + +import byteSize from 'byte-size' + +/** + * Creates a badge object that displays information about a size in bytes number. + * It should usually be used to output a size badge. + * + * @param {number} bytes - Raw number of bytes to be formatted + * @param {'metric'|'iec'} units - Either 'metric' (multiples of 1000) or 'iec' (multiples of 1024). + * This should align with how the upstream displays sizes. + * @param {string} [label='size'] - Custom label + * @returns {object} A badge object that has three properties: label, message, and color + */ +function renderSizeBadge(bytes, units, label = 'size') { + return { + label, + message: byteSize(bytes, { units }).toString(), + color: 'blue', + } +} + +export { renderSizeBadge } diff --git a/services/spiget/spiget-download-size.tester.js b/services/spiget/spiget-download-size.tester.js index 9f542d594b255..aba190e865c69 100644 --- a/services/spiget/spiget-download-size.tester.js +++ b/services/spiget/spiget-download-size.tester.js @@ -1,10 +1,10 @@ -import { isFileSize } from '../test-validators.js' +import { isMetricFileSize } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('EssentialsX (hosted resource)') .get('/771.json') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isMetricFileSize }) t.create('external resource').get('/9089.json').expectBadge({ label: 'size', diff --git a/services/steam/steam-workshop.service.js b/services/steam/steam-workshop.service.js index cfbc02a97c031..a1024f81e7363 100644 --- a/services/steam/steam-workshop.service.js +++ b/services/steam/steam-workshop.service.js @@ -1,6 +1,6 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' import { renderDateBadge } from '../date.js' +import { renderSizeBadge } from '../size.js' import { renderDownloadsBadge } from '../downloads.js' import { metric } from '../text-formatters.js' import { NotFound, pathParams } from '../index.js' @@ -208,12 +208,8 @@ class SteamFileSize extends SteamFileService { label: 'size', } - static render({ fileSize }) { - return { message: prettyBytes(fileSize), color: 'informational' } - } - async onRequest({ response }) { - return this.constructor.render({ fileSize: response.file_size }) + return renderSizeBadge(response.file_size, 'metric') } } diff --git a/services/steam/steam-workshop.tester.js b/services/steam/steam-workshop.tester.js index 05b424ac9d658..578551e222990 100644 --- a/services/steam/steam-workshop.tester.js +++ b/services/steam/steam-workshop.tester.js @@ -1,5 +1,9 @@ import { ServiceTester } from '../tester.js' -import { isMetric, isFileSize, isFormattedDate } from '../test-validators.js' +import { + isMetric, + isMetricFileSize, + isFormattedDate, +} from '../test-validators.js' export const t = new ServiceTester({ id: 'steam', @@ -12,7 +16,7 @@ t.create('Collection Files') t.create('File Size') .get('/size/1523924535.json') - .expectBadge({ label: 'size', message: isFileSize }) + .expectBadge({ label: 'size', message: isMetricFileSize }) t.create('Release Date') .get('/release-date/1523924535.json') diff --git a/services/test-validators.js b/services/test-validators.js index 34339fadd24e8..3f8fb50178856 100644 --- a/services/test-validators.js +++ b/services/test-validators.js @@ -106,9 +106,12 @@ const isPercentage = Joi.alternatives().try( isDecimalPercentageNegative, ) -const isFileSize = withRegex( +const isMetricFileSize = withRegex( /^[0-9]*[.]?[0-9]+\s(B|kB|KB|MB|GB|TB|PB|EB|ZB|YB)$/, ) +const isIecFileSize = withRegex( + /^[0-9]*[.]?[0-9]+\s(B|KiB|MiB|GiB|TiB|PiB|EiB|ZiB|YiB)$/, +) const isFormattedDate = Joi.alternatives().try( Joi.equal('today', 'yesterday'), @@ -202,7 +205,8 @@ export { isPercentage, isIntegerPercentage, isDecimalPercentage, - isFileSize, + isMetricFileSize, + isIecFileSize, isFormattedDate, isRelativeFormattedDate, isDependencyState, diff --git a/services/visual-studio-app-center/visual-studio-app-center-releases-size.service.js b/services/visual-studio-app-center/visual-studio-app-center-releases-size.service.js index 0c1600c7b359f..bea5173d197b4 100644 --- a/services/visual-studio-app-center/visual-studio-app-center-releases-size.service.js +++ b/services/visual-studio-app-center/visual-studio-app-center-releases-size.service.js @@ -1,6 +1,6 @@ import Joi from 'joi' -import prettyBytes from 'pretty-bytes' import { pathParams } from '../index.js' +import { renderSizeBadge } from '../size.js' import { nonNegativeInteger } from '../validators.js' import { BaseVisualStudioAppCenterService, @@ -47,14 +47,8 @@ export default class VisualStudioAppCenterReleasesSize extends BaseVisualStudioA color: 'blue', } - static render({ size }) { - return { - message: prettyBytes(size), - } - } - async handle({ owner, app, token }) { const { size } = await this.fetch({ owner, app, token, schema }) - return this.constructor.render({ size }) + return renderSizeBadge(size, 'metric') } } diff --git a/services/visual-studio-app-center/visual-studio-app-center-releases-size.tester.js b/services/visual-studio-app-center/visual-studio-app-center-releases-size.tester.js index 9f41c810f9e05..b6780f19cd653 100644 --- a/services/visual-studio-app-center/visual-studio-app-center-releases-size.tester.js +++ b/services/visual-studio-app-center/visual-studio-app-center-releases-size.tester.js @@ -1,5 +1,5 @@ import { createServiceTester } from '../tester.js' -import { isFileSize } from '../test-validators.js' +import { isMetricFileSize } from '../test-validators.js' export const t = await createServiceTester() t.create('8368844 bytes to 8.37 megabytes') @@ -13,7 +13,7 @@ t.create('8368844 bytes to 8.37 megabytes') ) .expectBadge({ label: 'size', - message: '8.37 MB', + message: '8.4 MB', }) t.create('Valid Release') @@ -22,7 +22,7 @@ t.create('Valid Release') ) .expectBadge({ label: 'size', - message: isFileSize, + message: isMetricFileSize, }) t.create('Valid user, invalid project, valid API token') diff --git a/services/whatpulse/whatpulse.tester.js b/services/whatpulse/whatpulse.tester.js index c8850805d4549..4c0b3e47f118d 100644 --- a/services/whatpulse/whatpulse.tester.js +++ b/services/whatpulse/whatpulse.tester.js @@ -1,6 +1,6 @@ import { createServiceTester } from '../tester.js' import { - isFileSize, + isMetricFileSize, isHumanized, isMetric, isOrdinalNumber, @@ -21,11 +21,11 @@ t.create('WhatPulse team as team id, clicks') t.create('WhatPulse team as team id, download') .get('/download/team/1295.json') - .expectBadge({ label: 'download', message: isFileSize }) + .expectBadge({ label: 'download', message: isMetricFileSize }) t.create('WhatPulse team as team id, upload') .get('/upload/team/1295.json') - .expectBadge({ label: 'upload', message: isFileSize }) + .expectBadge({ label: 'upload', message: isMetricFileSize }) t.create('WhatPulse team as team name, keys - from Ranks') .get('/keys/team/dutch power cows.json?rank')