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
This commit is contained in:
chris48s
2024-12-01 19:53:26 +00:00
committed by GitHub
parent b7d7f4545d
commit 151c70dd17
28 changed files with 142 additions and 144 deletions

21
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@shields_io/camp": "^18.1.2", "@shields_io/camp": "^18.1.2",
"@xmldom/xmldom": "0.9.5", "@xmldom/xmldom": "0.9.5",
"badge-maker": "file:badge-maker", "badge-maker": "file:badge-maker",
"byte-size": "^9.0.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"camelcase": "^8.0.0", "camelcase": "^8.0.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
@@ -45,7 +46,6 @@
"parse-link-header": "^2.0.0", "parse-link-header": "^2.0.0",
"path-to-regexp": "^6.3.0", "path-to-regexp": "^6.3.0",
"pg": "^8.13.1", "pg": "^8.13.1",
"pretty-bytes": "^6.1.1",
"priorityqueuejs": "^2.0.0", "priorityqueuejs": "^2.0.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"qs": "^6.13.1", "qs": "^6.13.1",
@@ -8132,6 +8132,14 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -24578,17 +24586,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "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": { "node_modules/pretty-error": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",

View File

@@ -27,6 +27,7 @@
"@shields_io/camp": "^18.1.2", "@shields_io/camp": "^18.1.2",
"@xmldom/xmldom": "0.9.5", "@xmldom/xmldom": "0.9.5",
"badge-maker": "file:badge-maker", "badge-maker": "file:badge-maker",
"byte-size": "^9.0.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"camelcase": "^8.0.0", "camelcase": "^8.0.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
@@ -57,7 +58,6 @@
"parse-link-header": "^2.0.0", "parse-link-header": "^2.0.0",
"path-to-regexp": "^6.3.0", "path-to-regexp": "^6.3.0",
"pg": "^8.13.1", "pg": "^8.13.1",
"pretty-bytes": "^6.1.1",
"priorityqueuejs": "^2.0.0", "priorityqueuejs": "^2.0.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"qs": "^6.13.1", "qs": "^6.13.1",

View File

@@ -1,9 +1,11 @@
import Joi from 'joi' import Joi from 'joi'
import { BaseJsonService, pathParam, queryParam } from '../index.js' import { BaseJsonService, pathParam, queryParam } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js'
const schema = Joi.object({ const schema = Joi.object({
size: Joi.object({ size: Joi.object({
compressedSize: Joi.string().required(), rawCompressedSize: nonNegativeInteger,
}).required(), }).required(),
}).required() }).required()
@@ -76,13 +78,6 @@ export default class BundlejsPackage extends BaseJsonService {
static defaultBadgeData = { label: 'bundlejs', color: 'informational' } static defaultBadgeData = { label: 'bundlejs', color: 'informational' }
static render({ size }) {
return {
label: 'minified size (gzip)',
message: size,
}
}
async fetch({ scope, packageName, exports }) { async fetch({ scope, packageName, exports }) {
const searchParams = { const searchParams = {
q: `${scope ? `${scope}/` : ''}${packageName}`, q: `${scope ? `${scope}/` : ''}${packageName}`,
@@ -110,7 +105,7 @@ export default class BundlejsPackage extends BaseJsonService {
async handle({ scope, packageName }, { exports }) { async handle({ scope, packageName }, { exports }) {
const json = await this.fetch({ scope, packageName, exports }) const json = await this.fetch({ scope, packageName, exports })
const size = json.size.compressedSize const size = json.size.rawCompressedSize
return this.constructor.render({ size }) return renderSizeBadge(size, 'metric', 'minified size (gzip)')
} }
} }

View File

@@ -1,26 +1,26 @@
import { isFileSize } from '../test-validators.js' import { isMetricFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('bundlejs/package (packageName)') t.create('bundlejs/package (packageName)')
.get('/jquery.json') .get('/jquery.json')
.expectBadge({ label: 'minified size (gzip)', message: isFileSize }) .expectBadge({ label: 'minified size (gzip)', message: isMetricFileSize })
t.create('bundlejs/package (version)') t.create('bundlejs/package (version)')
.get('/react@18.2.0.json') .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)') t.create('bundlejs/package (scoped)')
.get('/@cycle/rx-run.json') .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)') t.create('bundlejs/package (select exports)')
.get('/value-enhancer.json?exports=isVal,val') .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)') t.create('bundlejs/package (scoped version select exports)')
.get('/@ngneat/falso@6.4.0.json?exports=randEmail,randFullName') .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)') t.create('bundlejs/package (not found)')
.get('/react@18.2.0.json') .get('/react@18.2.0.json')

View File

@@ -1,5 +1,5 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes' import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js' import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService, pathParams } from '../index.js' import { BaseJsonService, pathParams } from '../index.js'
@@ -112,10 +112,7 @@ export default class Bundlephobia extends BaseJsonService {
static render({ format, size }) { static render({ format, size }) {
const label = format === 'min' ? 'minified size' : 'minzipped size' const label = format === 'min' ? 'minified size' : 'minzipped size'
return { return renderSizeBadge(size, 'iec', label)
label,
message: prettyBytes(size),
}
} }
async fetch({ scope, packageName, version }) { async fetch({ scope, packageName, version }) {

View File

@@ -1,4 +1,4 @@
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
@@ -13,42 +13,42 @@ const data = [
{ {
format: formats.A, format: formats.A,
get: '/min/preact.json', get: '/min/preact.json',
expect: { label: 'minified size', message: isFileSize }, expect: { label: 'minified size', message: isIecFileSize },
}, },
{ {
format: formats.B, format: formats.B,
get: '/min/preact/8.0.0.json', get: '/min/preact/8.0.0.json',
expect: { label: 'minified size', message: isFileSize }, expect: { label: 'minified size', message: isIecFileSize },
}, },
{ {
format: formats.C, format: formats.C,
get: '/min/@cycle/core.json', get: '/min/@cycle/core.json',
expect: { label: 'minified size', message: isFileSize }, expect: { label: 'minified size', message: isIecFileSize },
}, },
{ {
format: formats.D, format: formats.D,
get: '/min/@cycle/core/7.0.0.json', get: '/min/@cycle/core/7.0.0.json',
expect: { label: 'minified size', message: isFileSize }, expect: { label: 'minified size', message: isIecFileSize },
}, },
{ {
format: formats.A, format: formats.A,
get: '/minzip/preact.json', get: '/minzip/preact.json',
expect: { label: 'minzipped size', message: isFileSize }, expect: { label: 'minzipped size', message: isIecFileSize },
}, },
{ {
format: formats.B, format: formats.B,
get: '/minzip/preact/8.0.0.json', get: '/minzip/preact/8.0.0.json',
expect: { label: 'minzipped size', message: isFileSize }, expect: { label: 'minzipped size', message: isIecFileSize },
}, },
{ {
format: formats.C, format: formats.C,
get: '/minzip/@cycle/core.json', get: '/minzip/@cycle/core.json',
expect: { label: 'minzipped size', message: isFileSize }, expect: { label: 'minzipped size', message: isIecFileSize },
}, },
{ {
format: formats.D, format: formats.D,
get: '/minzip/@cycle/core/7.0.0.json', get: '/minzip/@cycle/core/7.0.0.json',
expect: { label: 'minzipped size', message: isFileSize }, expect: { label: 'minzipped size', message: isIecFileSize },
}, },
{ {
format: formats.A, format: formats.A,

View File

@@ -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' import BaseChromeWebStoreService from './chrome-web-store-base.js'
export default class ChromeWebStoreSize extends BaseChromeWebStoreService { export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
@@ -22,6 +22,17 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
color: 'blue', 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 }) { async handle({ storeId }) {
const chromeWebStore = await this.fetch({ storeId }) const chromeWebStore = await this.fetch({ storeId })
const size = chromeWebStore.size() const size = chromeWebStore.size()
@@ -30,6 +41,6 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
throw new NotFound({ prettyMessage: 'not found' }) throw new NotFound({ prettyMessage: 'not found' })
} }
return { message: size } return { message: this.transform(size) }
} }
} }

View File

@@ -1,11 +1,11 @@
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
import { isIecFileSize } from '../test-validators.js'
export const t = await createServiceTester() export const t = await createServiceTester()
const isFileSize = /^\d+(\.\d+)?(MiB|KiB)$/
t.create('Size').get('/nccfelhkfpbnefflolffkclhenplhiab.json').expectBadge({ t.create('Size').get('/nccfelhkfpbnefflolffkclhenplhiab.json').expectBadge({
label: 'extension size', label: 'extension size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('Size (not found)') t.create('Size (not found)')

View File

@@ -1,5 +1,5 @@
import prettyBytes from 'pretty-bytes'
import { pathParams } from '../index.js' import { pathParams } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { BaseCratesService, description } from './crates-base.js' import { BaseCratesService, description } from './crates-base.js'
export default class CratesSize extends BaseCratesService { 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 }) { async handle({ crate, version }) {
const json = await this.fetch({ crate, version }) const json = await this.fetch({ crate, version })
const size = this.constructor.getVersionObj(json).crate_size const size = this.constructor.getVersionObj(json).crate_size
return this.render({ size }) return renderSizeBadge(size, 'iec')
} }
} }

View File

@@ -1,14 +1,14 @@
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('size') t.create('size')
.get('/tokio.json') .get('/tokio.json')
.expectBadge({ label: 'size', message: isFileSize }) .expectBadge({ label: 'size', message: isIecFileSize })
t.create('size (with version)') t.create('size (with version)')
.get('/tokio/1.32.0.json') .get('/tokio/1.32.0.json')
.expectBadge({ label: 'size', message: '725 kB' }) .expectBadge({ label: 'size', message: '708 KiB' })
t.create('size (not found)') t.create('size (not found)')
.get('/not-a-crate.json') .get('/not-a-crate.json')

View File

@@ -1,5 +1,5 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes' import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js' import { nonNegativeInteger } from '../validators.js'
import { latest } from '../version.js' import { latest } from '../version.js'
import { BaseJsonService, NotFound, pathParams, queryParams } from '../index.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 defaultBadgeData = { label: 'image size', color: 'blue' }
static render({ size }) {
return { message: prettyBytes(size) }
}
async fetch({ user, repo, tag, page }) { async fetch({ user, repo, tag, page }) {
page = page ? `&page=${page}` : '' page = page ? `&page=${page}` : ''
return await fetch(this, { return await fetch(this, {
@@ -233,6 +229,6 @@ export default class DockerSize extends BaseJsonService {
} }
const { size } = await this.transform({ tag, sort, data, arch }) const { size } = await this.transform({ tag, sort, data, arch })
return this.constructor.render({ size }) return renderSizeBadge(size, 'iec', 'image size')
} }
} }

View File

@@ -1,4 +1,4 @@
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
@@ -6,35 +6,35 @@ t.create('docker image size (valid, library)')
.get('/_/alpine.json') .get('/_/alpine.json')
.expectBadge({ .expectBadge({
label: 'image size', label: 'image size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('docker image size (valid, library, arch parameter )') t.create('docker image size (valid, library, arch parameter )')
.get('/_/mysql.json?arch=amd64') .get('/_/mysql.json?arch=amd64')
.expectBadge({ .expectBadge({
label: 'image size', label: 'image size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('docker image size (valid, library with tag)') t.create('docker image size (valid, library with tag)')
.get('/_/alpine/latest.json') .get('/_/alpine/latest.json')
.expectBadge({ .expectBadge({
label: 'image size', label: 'image size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('docker image size (valid, user)') t.create('docker image size (valid, user)')
.get('/jrottenberg/ffmpeg.json') .get('/jrottenberg/ffmpeg.json')
.expectBadge({ .expectBadge({
label: 'image size', label: 'image size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('docker image size (valid, user with tag)') t.create('docker image size (valid, user with tag)')
.get('/jrottenberg/ffmpeg/3.2-alpine.json') .get('/jrottenberg/ffmpeg/3.2-alpine.json')
.expectBadge({ .expectBadge({
label: 'image size', label: 'image size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('docker image size (invalid, incorrect tag)') t.create('docker image size (invalid, incorrect tag)')

View File

@@ -1,5 +1,5 @@
import prettyBytes from 'pretty-bytes'
import { pathParams } from '../index.js' import { pathParams } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { BaseGithubLanguage } from './github-languages-base.js' import { BaseGithubLanguage } from './github-languages-base.js'
import { documentation } from './github-helpers.js' import { documentation } from './github-helpers.js'
@@ -31,15 +31,8 @@ export default class GithubCodeSize extends BaseGithubLanguage {
static defaultBadgeData = { label: 'code size' } static defaultBadgeData = { label: 'code size' }
static render({ size }) {
return {
message: prettyBytes(size),
color: 'blue',
}
}
async handle({ user, repo }) { async handle({ user, repo }) {
const data = await this.fetch({ 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')
} }
} }

View File

@@ -1,4 +1,4 @@
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
@@ -6,7 +6,7 @@ t.create('code size in bytes for all languages')
.get('/badges/shields.json') .get('/badges/shields.json')
.expectBadge({ .expectBadge({
label: 'code size', label: 'code size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('code size in bytes for all languages (empty repo)') t.create('code size in bytes for all languages (empty repo)')

View File

@@ -1,6 +1,6 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes'
import { pathParams } from '../index.js' import { pathParams } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js' import { nonNegativeInteger } from '../validators.js'
import { GithubAuthV3Service } from './github-auth-service.js' import { GithubAuthV3Service } from './github-auth-service.js'
import { documentation, httpErrorsFor } from './github-helpers.js' import { documentation, httpErrorsFor } from './github-helpers.js'
@@ -33,14 +33,6 @@ export default class GithubRepoSize extends GithubAuthV3Service {
static defaultBadgeData = { label: 'repo size' } 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 }) { async fetch({ user, repo }) {
return this._requestJson({ return this._requestJson({
url: `/repos/${user}/${repo}`, url: `/repos/${user}/${repo}`,
@@ -51,6 +43,8 @@ export default class GithubRepoSize extends GithubAuthV3Service {
async handle({ user, repo }) { async handle({ user, repo }) {
const { size } = await this.fetch({ 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')
} }
} }

View File

@@ -1,10 +1,10 @@
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('repository size').get('/badges/shields.json').expectBadge({ t.create('repository size').get('/badges/shields.json').expectBadge({
label: 'repo size', label: 'repo size',
message: isFileSize, message: isIecFileSize,
}) })
t.create('repository size (repo not found)') t.create('repository size (repo not found)')

View File

@@ -1,5 +1,5 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes' import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js' import { nonNegativeInteger } from '../validators.js'
import { NotFound, pathParam, queryParam } from '../index.js' import { NotFound, pathParam, queryParam } from '../index.js'
import { GithubAuthV3Service } from './github-auth-service.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 }) { async fetch({ user, repo, path, branch }) {
if (branch) { if (branch) {
return this._requestJson({ return this._requestJson({
@@ -73,6 +66,6 @@ export default class GithubSize extends GithubAuthV3Service {
if (Array.isArray(body)) { if (Array.isArray(body)) {
throw new NotFound({ prettyMessage: 'not a regular file' }) throw new NotFound({ prettyMessage: 'not a regular file' })
} }
return this.constructor.render({ size: body.size }) return renderSizeBadge(body.size, 'iec')
} }
} }

View File

@@ -1,10 +1,10 @@
import { isFileSize } from '../test-validators.js' import { isIecFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('File size') t.create('File size')
.get('/webcaetano/craft/build/phaser-craft.min.js.json') .get('/webcaetano/craft/build/phaser-craft.min.js.json')
.expectBadge({ label: 'size', message: isFileSize }) .expectBadge({ label: 'size', message: isIecFileSize })
t.create('File size 404') t.create('File size 404')
.get('/webcaetano/craft/build/does-not-exist.min.js.json') .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') t.create('File size for a specified branch')
.get('/webcaetano/craft/build/craft.min.js.json?branch=version-2') .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') t.create('File size for a specified tag')
.get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=2.1.2') .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') t.create('File size for a specified commit')
.get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=b848dbb') .get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=b848dbb')
.expectBadge({ label: 'size', message: isFileSize }) .expectBadge({ label: 'size', message: isIecFileSize })

View File

@@ -1,8 +1,11 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes'
import { pathParam, queryParam } from '../index.js' import { pathParam, queryParam } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { optionalNonNegativeInteger } from '../validators.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({ const schema = Joi.object({
dist: Joi.object({ dist: Joi.object({
@@ -16,6 +19,7 @@ export default class NpmUnpackedSize extends NpmBase {
static route = { static route = {
base: 'npm/unpacked-size', base: 'npm/unpacked-size',
pattern: ':scope(@[^/]+)?/:packageName/:version*', pattern: ':scope(@[^/]+)?/:packageName/:version*',
queryParamSchema,
} }
static openApi = { static openApi = {
@@ -78,10 +82,13 @@ export default class NpmUnpackedSize extends NpmBase {
}) })
const { unpackedSize } = dist const { unpackedSize } = dist
if (unpackedSize) {
return renderSizeBadge(unpackedSize, 'metric', 'unpacked size')
}
return { return {
label: 'unpacked size', label: 'unpacked size',
message: unpackedSize ? prettyBytes(unpackedSize) : 'unknown', message: 'unknown',
color: unpackedSize ? 'blue' : 'lightgray', color: 'lightgray',
} }
} }
} }

View File

@@ -1,11 +1,11 @@
import { isFileSize } from '../test-validators.js' import { isMetricFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('Latest unpacked size') t.create('Latest unpacked size')
.get('/firereact.json') .get('/firereact.json')
.expectBadge({ label: 'unpacked size', message: isFileSize }) .expectBadge({ label: 'unpacked size', message: isMetricFileSize })
t.create('Nonexistent unpacked size with version') t.create('Nonexistent unpacked size with version')
.get('/express/4.16.0.json') .get('/express/4.16.0.json')
@@ -13,15 +13,15 @@ t.create('Nonexistent unpacked size with version')
t.create('Unpacked size with version') t.create('Unpacked size with version')
.get('/firereact/0.7.0.json') .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') t.create('Unpacked size for scoped package')
.get('/@testing-library/react.json') .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') t.create('Unpacked size for scoped package with version')
.get('/@testing-library/react/14.2.1.json') .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') t.create('Nonexistent unpacked size for scoped package with version')
.get('/@cycle/rx-run/7.2.0.json') .get('/@cycle/rx-run/7.2.0.json')

25
services/size.js Normal file
View File

@@ -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 }

View File

@@ -1,10 +1,10 @@
import { isFileSize } from '../test-validators.js' import { isMetricFileSize } from '../test-validators.js'
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('EssentialsX (hosted resource)') t.create('EssentialsX (hosted resource)')
.get('/771.json') .get('/771.json')
.expectBadge({ label: 'size', message: isFileSize }) .expectBadge({ label: 'size', message: isMetricFileSize })
t.create('external resource').get('/9089.json').expectBadge({ t.create('external resource').get('/9089.json').expectBadge({
label: 'size', label: 'size',

View File

@@ -1,6 +1,6 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes'
import { renderDateBadge } from '../date.js' import { renderDateBadge } from '../date.js'
import { renderSizeBadge } from '../size.js'
import { renderDownloadsBadge } from '../downloads.js' import { renderDownloadsBadge } from '../downloads.js'
import { metric } from '../text-formatters.js' import { metric } from '../text-formatters.js'
import { NotFound, pathParams } from '../index.js' import { NotFound, pathParams } from '../index.js'
@@ -208,12 +208,8 @@ class SteamFileSize extends SteamFileService {
label: 'size', label: 'size',
} }
static render({ fileSize }) {
return { message: prettyBytes(fileSize), color: 'informational' }
}
async onRequest({ response }) { async onRequest({ response }) {
return this.constructor.render({ fileSize: response.file_size }) return renderSizeBadge(response.file_size, 'metric')
} }
} }

View File

@@ -1,5 +1,9 @@
import { ServiceTester } from '../tester.js' 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({ export const t = new ServiceTester({
id: 'steam', id: 'steam',
@@ -12,7 +16,7 @@ t.create('Collection Files')
t.create('File Size') t.create('File Size')
.get('/size/1523924535.json') .get('/size/1523924535.json')
.expectBadge({ label: 'size', message: isFileSize }) .expectBadge({ label: 'size', message: isMetricFileSize })
t.create('Release Date') t.create('Release Date')
.get('/release-date/1523924535.json') .get('/release-date/1523924535.json')

View File

@@ -106,9 +106,12 @@ const isPercentage = Joi.alternatives().try(
isDecimalPercentageNegative, isDecimalPercentageNegative,
) )
const isFileSize = withRegex( const isMetricFileSize = withRegex(
/^[0-9]*[.]?[0-9]+\s(B|kB|KB|MB|GB|TB|PB|EB|ZB|YB)$/, /^[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( const isFormattedDate = Joi.alternatives().try(
Joi.equal('today', 'yesterday'), Joi.equal('today', 'yesterday'),
@@ -202,7 +205,8 @@ export {
isPercentage, isPercentage,
isIntegerPercentage, isIntegerPercentage,
isDecimalPercentage, isDecimalPercentage,
isFileSize, isMetricFileSize,
isIecFileSize,
isFormattedDate, isFormattedDate,
isRelativeFormattedDate, isRelativeFormattedDate,
isDependencyState, isDependencyState,

View File

@@ -1,6 +1,6 @@
import Joi from 'joi' import Joi from 'joi'
import prettyBytes from 'pretty-bytes'
import { pathParams } from '../index.js' import { pathParams } from '../index.js'
import { renderSizeBadge } from '../size.js'
import { nonNegativeInteger } from '../validators.js' import { nonNegativeInteger } from '../validators.js'
import { import {
BaseVisualStudioAppCenterService, BaseVisualStudioAppCenterService,
@@ -47,14 +47,8 @@ export default class VisualStudioAppCenterReleasesSize extends BaseVisualStudioA
color: 'blue', color: 'blue',
} }
static render({ size }) {
return {
message: prettyBytes(size),
}
}
async handle({ owner, app, token }) { async handle({ owner, app, token }) {
const { size } = await this.fetch({ owner, app, token, schema }) const { size } = await this.fetch({ owner, app, token, schema })
return this.constructor.render({ size }) return renderSizeBadge(size, 'metric')
} }
} }

View File

@@ -1,5 +1,5 @@
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
import { isFileSize } from '../test-validators.js' import { isMetricFileSize } from '../test-validators.js'
export const t = await createServiceTester() export const t = await createServiceTester()
t.create('8368844 bytes to 8.37 megabytes') t.create('8368844 bytes to 8.37 megabytes')
@@ -13,7 +13,7 @@ t.create('8368844 bytes to 8.37 megabytes')
) )
.expectBadge({ .expectBadge({
label: 'size', label: 'size',
message: '8.37 MB', message: '8.4 MB',
}) })
t.create('Valid Release') t.create('Valid Release')
@@ -22,7 +22,7 @@ t.create('Valid Release')
) )
.expectBadge({ .expectBadge({
label: 'size', label: 'size',
message: isFileSize, message: isMetricFileSize,
}) })
t.create('Valid user, invalid project, valid API token') t.create('Valid user, invalid project, valid API token')

View File

@@ -1,6 +1,6 @@
import { createServiceTester } from '../tester.js' import { createServiceTester } from '../tester.js'
import { import {
isFileSize, isMetricFileSize,
isHumanized, isHumanized,
isMetric, isMetric,
isOrdinalNumber, isOrdinalNumber,
@@ -21,11 +21,11 @@ t.create('WhatPulse team as team id, clicks')
t.create('WhatPulse team as team id, download') t.create('WhatPulse team as team id, download')
.get('/download/team/1295.json') .get('/download/team/1295.json')
.expectBadge({ label: 'download', message: isFileSize }) .expectBadge({ label: 'download', message: isMetricFileSize })
t.create('WhatPulse team as team id, upload') t.create('WhatPulse team as team id, upload')
.get('/upload/team/1295.json') .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') t.create('WhatPulse team as team name, keys - from Ranks')
.get('/keys/team/dutch power cows.json?rank') .get('/keys/team/dutch power cows.json?rank')