Compare commits
93 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e9322f29 | ||
|
|
bf91e268d6 | ||
|
|
85b44b9152 | ||
|
|
c567f6cde4 | ||
|
|
7108e08670 | ||
|
|
b4c21fd65d | ||
|
|
00c73c872d | ||
|
|
e6b66a8865 | ||
|
|
56b9c78b65 | ||
|
|
cb309028db | ||
|
|
7174c5ad17 | ||
|
|
8f8eff5e2f | ||
|
|
d4090a2665 | ||
|
|
53eab46666 | ||
|
|
2b90459bb4 | ||
|
|
0cbc1319ce | ||
|
|
ac01fdefcc | ||
|
|
3d6c438df2 | ||
|
|
a729e65393 | ||
|
|
c4c0c2c6f6 | ||
|
|
e4eb6cb6c4 | ||
|
|
c4b6b7923b | ||
|
|
d1058bc73d | ||
|
|
f643515ee1 | ||
|
|
21a059d9a8 | ||
|
|
7c067fdcbf | ||
|
|
f6fde2b78b | ||
|
|
1da19cffaa | ||
|
|
e1d0ab10ae | ||
|
|
e3f382114e | ||
|
|
ab16aa3c03 | ||
|
|
deaf85f6fc | ||
|
|
d481322a3f | ||
|
|
71cddb7abc | ||
|
|
1756cb834e | ||
|
|
fb816ecf93 | ||
|
|
075f1b450e | ||
|
|
d7bb13c0bc | ||
|
|
f79ed50c9b | ||
|
|
0d08f2ef50 | ||
|
|
a87e1c32fb | ||
|
|
f48a205032 | ||
|
|
6b6cc3ac70 | ||
|
|
ab7aaff60e | ||
|
|
e5dbfe7ea5 | ||
|
|
bed8a63d76 | ||
|
|
151c70dd17 | ||
|
|
b7d7f4545d | ||
|
|
2a20f813df | ||
|
|
3b465533fd | ||
|
|
df719ea2db | ||
|
|
9ee8ee8cbf | ||
|
|
def3007602 | ||
|
|
7d7f70b4b5 | ||
|
|
2ded4aa7b6 | ||
|
|
da2745d523 | ||
|
|
1338bf3192 | ||
|
|
e8e253d21e | ||
|
|
60aa530966 | ||
|
|
9447077c08 | ||
|
|
b60d738999 | ||
|
|
85fb206c8b | ||
|
|
d1210e2311 | ||
|
|
85dd5a599f | ||
|
|
e108e40930 | ||
|
|
eaa4317039 | ||
|
|
5cdef88bcc | ||
|
|
4132ca2e7e | ||
|
|
e1541acc11 | ||
|
|
f5dd749ae0 | ||
|
|
ef17850f7e | ||
|
|
5bde4266c8 | ||
|
|
e57edb42bd | ||
|
|
69f9251e1c | ||
|
|
39d3fd332b | ||
|
|
58f5b99fea | ||
|
|
20959b15db | ||
|
|
cbb7ab5e8b | ||
|
|
2bd926e65f | ||
|
|
04638ab0ee | ||
|
|
4d203e1937 | ||
|
|
6219c6da82 | ||
|
|
a16cf24b52 | ||
|
|
d3a1ef2ff7 | ||
|
|
32b1e341d7 | ||
|
|
2d5b72b207 | ||
|
|
0c4fed4dc6 | ||
|
|
8af909d118 | ||
|
|
00d72da97e | ||
|
|
4ec62fa445 | ||
|
|
57520a974f | ||
|
|
8c7872a666 | ||
|
|
ad82f7647a |
@@ -1,7 +0,0 @@
|
||||
/api-docs/
|
||||
/build
|
||||
/coverage
|
||||
/__snapshots__
|
||||
public
|
||||
badge-maker/node_modules/
|
||||
!.github/
|
||||
189
.eslintrc.yml
189
.eslintrc.yml
@@ -1,189 +0,0 @@
|
||||
extends:
|
||||
- standard
|
||||
- standard-jsx
|
||||
- standard-react
|
||||
- prettier
|
||||
- eslint:recommended
|
||||
|
||||
globals:
|
||||
JSX: 'readonly'
|
||||
|
||||
parserOptions:
|
||||
# Override eslint-config-standard, which incorrectly sets this to "module",
|
||||
# though that setting is only for ES6 modules, not CommonJS modules.
|
||||
sourceType: 'script'
|
||||
|
||||
settings:
|
||||
react:
|
||||
version: '16.8'
|
||||
jsdoc:
|
||||
mode: typescript
|
||||
|
||||
plugins:
|
||||
- chai-friendly
|
||||
- jsdoc
|
||||
- mocha
|
||||
- icedfrisby
|
||||
- no-extension-in-require
|
||||
- sort-class-members
|
||||
- import
|
||||
- react-hooks
|
||||
- promise
|
||||
|
||||
overrides:
|
||||
# For simplicity's sake, when possible prefer to add rules to the top-level
|
||||
# list of rules, even if they only apply to certain files. That way the
|
||||
# rules listed here are only ones which conflict.
|
||||
|
||||
- files:
|
||||
- 'badge-maker/**/*.js'
|
||||
- '**/*.cjs'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
|
||||
- files:
|
||||
- '**/*.js'
|
||||
- '!frontend/**/*.js'
|
||||
- '!badge-maker/**/*.js'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
rules:
|
||||
no-console: 'off'
|
||||
|
||||
- files:
|
||||
- '**/*.ts'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
|
||||
- files:
|
||||
- 'frontend/**/*.js'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
env:
|
||||
browser: true
|
||||
rules:
|
||||
import/extensions:
|
||||
['error', 'never', { 'json': 'always', 'yml': 'always' }]
|
||||
|
||||
- files:
|
||||
- 'core/base-service/**/*.js'
|
||||
- 'services/**/*.js'
|
||||
rules:
|
||||
sort-class-members/sort-class-members:
|
||||
[
|
||||
'error',
|
||||
{
|
||||
order:
|
||||
[
|
||||
'name',
|
||||
'category',
|
||||
'isDeprecated',
|
||||
'route',
|
||||
'auth',
|
||||
'examples',
|
||||
'_cacheLength',
|
||||
'defaultBadgeData',
|
||||
'render',
|
||||
'constructor',
|
||||
'fetch',
|
||||
'transform',
|
||||
'handle',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
- files:
|
||||
- '**/*.spec.@(js|ts|tsx)'
|
||||
- '**/*.integration.js'
|
||||
- '**/test-helpers.js'
|
||||
- 'core/service-test-runner/**/*.js'
|
||||
env:
|
||||
mocha: true
|
||||
rules:
|
||||
mocha/no-exclusive-tests: 'error'
|
||||
mocha/no-skipped-tests: 'error'
|
||||
mocha/no-mocha-arrows: 'error'
|
||||
mocha/prefer-arrow-callback: 'error'
|
||||
|
||||
- files:
|
||||
- 'services/**/*.tester.js'
|
||||
rules:
|
||||
icedfrisby/no-exclusive-tests: 'error'
|
||||
icedfrisby/no-skipped-tests: 'error'
|
||||
|
||||
rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
|
||||
no-use-before-define: 'off'
|
||||
|
||||
# These should be disabled by eslint-config-prettier, but are not.
|
||||
no-extra-semi: 'off'
|
||||
|
||||
# Shields additions.
|
||||
no-var: 'error'
|
||||
prefer-const: 'error'
|
||||
arrow-body-style: ['error', 'as-needed']
|
||||
no-extension-in-require/main: 'error'
|
||||
object-shorthand: ['error', 'properties']
|
||||
prefer-template: 'error'
|
||||
promise/prefer-await-to-then: 'error'
|
||||
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
|
||||
new-cap: ['error', { 'capIsNew': true }]
|
||||
import/order: ['error', { 'newlines-between': 'never' }]
|
||||
quotes:
|
||||
['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }]
|
||||
|
||||
# Account for destructuring responses from upstream services,
|
||||
# many of which do not follow camelcase
|
||||
# Based on original rule configuration from eslint-config-standard
|
||||
camelcase:
|
||||
[
|
||||
'error',
|
||||
{
|
||||
ignoreDestructuring: true,
|
||||
properties: 'never',
|
||||
ignoreGlobals: true,
|
||||
allow: ['^UNSAFE_'],
|
||||
},
|
||||
]
|
||||
|
||||
# Chai friendly.
|
||||
no-unused-expressions: 'off'
|
||||
chai-friendly/no-unused-expressions: 'error'
|
||||
|
||||
# jsdoc plugin:
|
||||
# don't require every class/function to have a docblock
|
||||
jsdoc/require-jsdoc: 'off'
|
||||
|
||||
# allow Joi as an undefined type
|
||||
jsdoc/no-undefined-types: ['error', { definedTypes: ['Joi'] }]
|
||||
|
||||
# all the other recommended rules as errors (not warnings)
|
||||
jsdoc/check-alignment: 'error'
|
||||
jsdoc/check-param-names: 'error'
|
||||
jsdoc/check-tag-names: 'error'
|
||||
jsdoc/check-types: 'error'
|
||||
jsdoc/implements-on-classes: 'error'
|
||||
jsdoc/tag-lines: ['error', 'any', { 'startLines': 1 }]
|
||||
jsdoc/require-param: 'error'
|
||||
jsdoc/require-param-description: 'error'
|
||||
jsdoc/require-param-name: 'error'
|
||||
jsdoc/require-param-type: 'error'
|
||||
jsdoc/require-returns: 'error'
|
||||
jsdoc/require-returns-check: 'error'
|
||||
jsdoc/require-returns-description: 'error'
|
||||
jsdoc/require-returns-type: 'error'
|
||||
jsdoc/valid-types: 'error'
|
||||
|
||||
react/prop-types: 'off'
|
||||
react/jsx-sort-props: 'error'
|
||||
react-hooks/rules-of-hooks: 'error'
|
||||
react-hooks/exhaustive-deps: 'error'
|
||||
jsx-quotes: ['error', 'prefer-double']
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -4,6 +4,28 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2025-01-01
|
||||
|
||||
- Add [PypiTypes] badge [#10774](https://github.com/badges/shields/issues/10774)
|
||||
- feat(endpoint-badge): add logoSize support [#10132](https://github.com/badges/shields/issues/10132)
|
||||
- fix auto-sized logo sizes [#10764](https://github.com/badges/shields/issues/10764)
|
||||
- Add [Coderabbit] PR Stats service and tests [#10749](https://github.com/badges/shields/issues/10749)
|
||||
- add [PUB] downloads badge [#10745](https://github.com/badges/shields/issues/10745)
|
||||
- Add [GitLab] Top Language Badge [#10750](https://github.com/badges/shields/issues/10750)
|
||||
- provide a non-repository scoped version of [githubcodesearch] [#10733](https://github.com/badges/shields/issues/10733)
|
||||
- [ReproducibleCentral] add Reproducible Central in Dependencies [#10705](https://github.com/badges/shields/issues/10705)
|
||||
- Add ability to format bytes as metric or IEC; affects [bundlejs bundlephobia ChromeWebStoreSize CratesSize DockerSize GithubRepoSize GithubCodeSize GithubSize NpmUnpackedSize SpigetDownloadSize steam VisualStudioAppCenterReleasesSize whatpulse] [#10547](https://github.com/badges/shields/issues/10547)
|
||||
- Dependency updates
|
||||
|
||||
## server-2024-12-01
|
||||
|
||||
- add [WingetVersion] Badge [#10245](https://github.com/badges/shields/issues/10245)
|
||||
- Fix broken URL for pingpong.one [#10655](https://github.com/badges/shields/issues/10655)
|
||||
- [npm] - Last update badge added [#10641](https://github.com/badges/shields/issues/10641)
|
||||
- reduce overhead of NPM Last Update badge; test [npm] [#10666](https://github.com/badges/shields/issues/10666)
|
||||
- Add YouTube-specific privacy notes [#10646](https://github.com/badges/shields/issues/10646)
|
||||
- Dependency updates
|
||||
|
||||
## server-2024-11-02
|
||||
|
||||
- cleanly handle null or undefined result from jsonpath-plus [#10645](https://github.com/badges/shields/issues/10645)
|
||||
|
||||
14
README.md
14
README.md
@@ -198,25 +198,19 @@ You can read more about [the project's inception][thread],
|
||||
|
||||
Maintainers:
|
||||
|
||||
- [calebcartwright](https://github.com/calebcartwright) (core team)
|
||||
- [chris48s](https://github.com/chris48s) (core team)
|
||||
- [Daniel15](https://github.com/Daniel15) (core team)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow) (core team)
|
||||
- [platan](https://github.com/platan) (core team)
|
||||
- [PyvesB](https://github.com/PyvesB) (core team)
|
||||
- [RedSparr0w](https://github.com/RedSparr0w) (core team)
|
||||
|
||||
Operations:
|
||||
|
||||
- [calebcartwright](https://github.com/calebcartwright)
|
||||
- [chris48s](https://github.com/chris48s)
|
||||
- [jNullj](https://github.com/jnullj)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow)
|
||||
- [PyvesB](https://github.com/PyvesB)
|
||||
|
||||
Alumni:
|
||||
|
||||
- [Daniel15](https://github.com/Daniel15)
|
||||
- [espadrine](https://github.com/espadrine)
|
||||
- [olivierlacan](https://github.com/olivierlacan)
|
||||
- [platan](https://github.com/platan)
|
||||
- [RedSparr0w](https://github.com/RedSparr0w)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const { spawn } = require('child-process-promise')
|
||||
const { expect, use } = require('chai')
|
||||
use(require('chai-string'))
|
||||
import path from 'path'
|
||||
import { spawn } from 'child-process-promise'
|
||||
import { expect, use } from 'chai'
|
||||
use(require('sinon-chai'))
|
||||
|
||||
function runCli(args) {
|
||||
@@ -15,7 +14,7 @@ function runCli(args) {
|
||||
describe('The CLI', function () {
|
||||
it('should provide a help message', async function () {
|
||||
const { stdout } = await runCli([])
|
||||
expect(stdout).to.startWith('Usage')
|
||||
expect(stdout.startsWith('Usage')).to.be.true
|
||||
})
|
||||
|
||||
it('should produce default badges', async function () {
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { makeBadge, ValidationError } = require('.')
|
||||
import { expect } from 'chai'
|
||||
import { makeBadge, ValidationError } from '.'
|
||||
|
||||
describe('makeBadge function', function () {
|
||||
it('should produce badge with valid input', async function () {
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const prettier = require('prettier')
|
||||
const makeBadge = require('./make-badge')
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import { expect } from 'chai'
|
||||
import snapshot from 'snap-shot-it'
|
||||
import prettier from 'prettier'
|
||||
import makeBadge from './make-badge'
|
||||
|
||||
async function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(await prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
@@ -65,6 +65,7 @@ const serviceDataSchema = Joi.object({
|
||||
namedLogo: Joi.string(),
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoSize: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
cacheSeconds: Joi.number().integer().min(0),
|
||||
style: Joi.string(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import chai from 'chai'
|
||||
import { expect, use } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import prometheus from 'prom-client'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
import BaseService from './base.js'
|
||||
import { MetricHelper, MetricNames } from './metric-helper.js'
|
||||
import '../register-chai-plugins.spec.js'
|
||||
const { expect } = chai
|
||||
chai.use(chaiAsPromised)
|
||||
use(chaiAsPromised)
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
queryParamA: Joi.string(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import chai, { expect } from 'chai'
|
||||
import { expect, use } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import httpMocks from 'node-mocks-http'
|
||||
import chaiDatetime from 'chai-datetime'
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
setCacheHeadersForStaticResource,
|
||||
serverHasBeenUpSinceResourceCached,
|
||||
} from './cache-headers.js'
|
||||
chai.use(chaiDatetime)
|
||||
use(chaiDatetime)
|
||||
|
||||
describe('Cache header functions', function () {
|
||||
let res
|
||||
|
||||
@@ -47,13 +47,13 @@ export default function coalesceBadge(
|
||||
label: overrideLabel,
|
||||
logo: overrideLogo,
|
||||
logoColor: overrideLogoColor,
|
||||
logoSize: overrideLogoSize,
|
||||
link: overrideLink,
|
||||
colorB: legacyOverrideColor,
|
||||
colorA: legacyOverrideLabelColor,
|
||||
} = overrides
|
||||
let {
|
||||
logoWidth: overrideLogoWidth,
|
||||
logoSize: overrideLogoSize,
|
||||
color: overrideColor,
|
||||
labelColor: overrideLabelColor,
|
||||
} = overrides
|
||||
|
||||
@@ -17,8 +17,14 @@ describe('mergeQueries function', function () {
|
||||
}
|
||||
`),
|
||||
),
|
||||
).to.equalIgnoreSpaces(
|
||||
'query ($param: String!) { foo(param: $param) { bar } }',
|
||||
).to.equal(
|
||||
print(gql`
|
||||
query ($param: String!) {
|
||||
foo(param: $param) {
|
||||
bar
|
||||
}
|
||||
}
|
||||
`),
|
||||
)
|
||||
|
||||
expect(
|
||||
@@ -38,8 +44,15 @@ describe('mergeQueries function', function () {
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces(
|
||||
'query ($param: String!) { foo(param: $param) { bar } baz }',
|
||||
).to.equal(
|
||||
print(gql`
|
||||
query ($param: String!) {
|
||||
foo(param: $param) {
|
||||
bar
|
||||
}
|
||||
baz
|
||||
}
|
||||
`),
|
||||
)
|
||||
|
||||
expect(
|
||||
@@ -62,7 +75,15 @@ describe('mergeQueries function', function () {
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces('{ foo bar baz }')
|
||||
).to.equal(
|
||||
print(gql`
|
||||
{
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
}
|
||||
`),
|
||||
)
|
||||
|
||||
expect(
|
||||
print(
|
||||
@@ -79,7 +100,14 @@ describe('mergeQueries function', function () {
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces('{ foo bar }')
|
||||
).to.equal(
|
||||
print(gql`
|
||||
{
|
||||
foo
|
||||
bar
|
||||
}
|
||||
`),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error when passed invalid params', function () {
|
||||
|
||||
@@ -115,7 +115,6 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
const result = handlerOptions.handler(
|
||||
filteredQueryParams,
|
||||
match,
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback
|
||||
function sendBadge(format, badgeData) {
|
||||
if (serverUnresponsive) {
|
||||
return
|
||||
@@ -128,7 +127,6 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
},
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
if (result && result.catch) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
result.catch(err => {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import chai from 'chai'
|
||||
import { expect, use } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import {
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
InvalidService,
|
||||
} from './loader.js'
|
||||
chai.use(chaiAsPromised)
|
||||
use(chaiAsPromised)
|
||||
|
||||
const { expect } = chai
|
||||
const fixturesDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'loader-test-fixtures',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import chai from 'chai'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
category2openapi,
|
||||
pathParam,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
queryParams,
|
||||
} from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
class OpenApiService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { use } from 'chai'
|
||||
import chaiString from 'chai-string'
|
||||
import sinonChai from 'sinon-chai'
|
||||
use(chaiString)
|
||||
use(sinonChai)
|
||||
|
||||
@@ -83,7 +83,7 @@ class ServiceTester {
|
||||
.before(() => {
|
||||
this.beforeEach()
|
||||
})
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback, promise/prefer-await-to-then
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.finally(function () {
|
||||
// `this` is the IcedFrisby instance.
|
||||
let responseBody
|
||||
|
||||
249
eslint.config.js
Normal file
249
eslint.config.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import chaiFriendlyPlugin from 'eslint-plugin-chai-friendly'
|
||||
import cypressPlugin from 'eslint-plugin-cypress/flat'
|
||||
import jsdocPlugin from 'eslint-plugin-jsdoc'
|
||||
import mochaPlugin from 'eslint-plugin-mocha'
|
||||
import icedfrisbyPlugin from 'eslint-plugin-icedfrisby'
|
||||
import sortClassMembersPlugin from 'eslint-plugin-sort-class-members'
|
||||
import importPlugin from 'eslint-plugin-import'
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks'
|
||||
import prettierConfig from 'eslint-plugin-prettier/recommended'
|
||||
import promisePlugin from 'eslint-plugin-promise'
|
||||
import globals from 'globals'
|
||||
import neostandard from 'neostandard'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import js from '@eslint/js'
|
||||
|
||||
// Config that is used across the whole codebase
|
||||
// and customisations to built-in ESLint rules
|
||||
const globalConfig = {
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
promise: promisePlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'import/order': ['error', { 'newlines-between': 'never' }],
|
||||
'promise/prefer-await-to-then': 'error',
|
||||
|
||||
// ESLint built-in rules config
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'object-shorthand': ['error', 'properties'],
|
||||
'prefer-template': 'error',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'new-cap': ['error', { capIsNew: true }],
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
{ avoidEscape: true, allowTemplateLiterals: false },
|
||||
],
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
ignoreDestructuring: true,
|
||||
properties: 'never',
|
||||
ignoreGlobals: true,
|
||||
allow: ['^UNSAFE_'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Node (CommonJS) files
|
||||
const commonJsConfig = {
|
||||
files: ['badge-maker/**/*.js', '**/*.cjs'],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Node (ESModules) files
|
||||
const nodeEsmConfig = {
|
||||
files: ['**/*.@(js|mjs)', '!frontend/**/*.js', '!badge-maker/**/*.js'],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
parser: tsParser,
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Frontend (ESModules) files
|
||||
const frontendConfig = {
|
||||
files: ['frontend/**/*.js'],
|
||||
|
||||
plugins: {
|
||||
'react-hooks': reactHooksPlugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Services
|
||||
const servicesConfig = {
|
||||
files: ['core/base-service/**/*.js', 'services/**/*.js'],
|
||||
|
||||
plugins: {
|
||||
'sort-class-members': sortClassMembersPlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'sort-class-members/sort-class-members': [
|
||||
'error',
|
||||
{
|
||||
order: [
|
||||
'name',
|
||||
'category',
|
||||
'isDeprecated',
|
||||
'route',
|
||||
'auth',
|
||||
'openApi',
|
||||
'_cacheLength',
|
||||
'defaultBadgeData',
|
||||
'render',
|
||||
'constructor',
|
||||
'fetch',
|
||||
'transform',
|
||||
'handle',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Mocha tests
|
||||
const mochaConfig = {
|
||||
files: [
|
||||
'**/*.spec.@(js|mjs|ts)',
|
||||
'**/*.integration.js',
|
||||
'**/test-helpers.js',
|
||||
'core/service-test-runner/**/*.js',
|
||||
],
|
||||
|
||||
plugins: {
|
||||
mocha: mochaPlugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mocha,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'mocha/no-exclusive-tests': 'error',
|
||||
'mocha/no-skipped-tests': 'error',
|
||||
'mocha/no-mocha-arrows': 'error',
|
||||
'mocha/prefer-arrow-callback': 'error',
|
||||
'no-unused-expressions': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting Cypress tests
|
||||
const cypressConfig = {
|
||||
files: ['**/*.cy.@(js|ts)'],
|
||||
...cypressPlugin.configs.recommended,
|
||||
}
|
||||
// append these to cypress.configs.recommended, without overwriting
|
||||
cypressConfig.plugins.mocha = mochaPlugin
|
||||
cypressConfig.rules['mocha/no-exclusive-tests'] = 'error'
|
||||
cypressConfig.rules['mocha/no-skipped-tests'] = 'error'
|
||||
cypressConfig.rules['mocha/no-mocha-arrows'] = 'off'
|
||||
|
||||
// config specific to linting Service tests (IcedFrisby)
|
||||
const serviceTestsConfig = {
|
||||
files: ['services/**/*.tester.js'],
|
||||
|
||||
plugins: {
|
||||
icedfrisby: icedfrisbyPlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'icedfrisby/no-exclusive-tests': 'error',
|
||||
'icedfrisby/no-skipped-tests': 'error',
|
||||
'no-unused-expressions': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
// config specific to linting JSDoc comments
|
||||
const jsDocConfig = {
|
||||
plugins: {
|
||||
jsdoc: jsdocPlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'jsdoc/no-undefined-types': ['error', { definedTypes: ['Joi'] }],
|
||||
'jsdoc/check-alignment': 'error',
|
||||
'jsdoc/check-param-names': 'error',
|
||||
'jsdoc/check-tag-names': 'error',
|
||||
'jsdoc/check-types': 'error',
|
||||
'jsdoc/implements-on-classes': 'error',
|
||||
'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }],
|
||||
'jsdoc/require-param': 'error',
|
||||
'jsdoc/require-param-description': 'error',
|
||||
'jsdoc/require-param-name': 'error',
|
||||
'jsdoc/require-param-type': 'error',
|
||||
'jsdoc/require-returns': 'error',
|
||||
'jsdoc/require-returns-check': 'error',
|
||||
'jsdoc/require-returns-description': 'error',
|
||||
'jsdoc/require-returns-type': 'error',
|
||||
'jsdoc/valid-types': 'error',
|
||||
},
|
||||
}
|
||||
|
||||
const config = [
|
||||
{
|
||||
ignores: [
|
||||
'api-docs/',
|
||||
'build',
|
||||
'coverage',
|
||||
'__snapshots__',
|
||||
'public',
|
||||
'badge-maker/node_modules/',
|
||||
'!.github/',
|
||||
'frontend/.docusaurus/**',
|
||||
],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
chaiFriendlyPlugin.configs.recommendedFlat,
|
||||
...neostandard({ noStyle: true }),
|
||||
|
||||
globalConfig,
|
||||
commonJsConfig,
|
||||
nodeEsmConfig,
|
||||
frontendConfig,
|
||||
servicesConfig,
|
||||
mochaConfig,
|
||||
cypressConfig,
|
||||
serviceTestsConfig,
|
||||
jsDocConfig,
|
||||
|
||||
// register prettierConfig last, as per
|
||||
// https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs
|
||||
prettierConfig,
|
||||
]
|
||||
|
||||
export default config
|
||||
2
fly.toml
2
fly.toml
@@ -25,6 +25,8 @@ processes = []
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
auto_stop_machines = "suspend"
|
||||
auto_start_machines = true
|
||||
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
|
||||
28
frontend/blog/2024-11-14-token-pool.md
Normal file
28
frontend/blog/2024-11-14-token-pool.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
slug: token-pool
|
||||
title: How shields.io uses the GitHub API
|
||||
authors:
|
||||
name: chris48s
|
||||
title: Shields.io Core Team
|
||||
url: https://github.com/chris48s
|
||||
image_url: https://avatars.githubusercontent.com/u/6025893
|
||||
tags: []
|
||||
---
|
||||
|
||||
We serve a lot of badges which display information fetched from the GitHub API. When I say a lot, this varies a bit but in a typical hour we make hundreds of thousands of calls to the GitHub API.
|
||||
|
||||
But hang on. GitHub's API has rate limits.
|
||||
|
||||
Specifically, users can make up to [5,000 requests per hour](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-authenticated-users) to GitHub's v3/REST API. The v4/GraphQL also applies rate limits, but it is based on a slightly more complicated [points-based system](https://docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api#primary-rate-limit).
|
||||
|
||||
In any case, we are clearly making many times more requests to GitHub's API than would be allowed with a single token.
|
||||
|
||||
So how are we doing that? Well, we have lots of tokens. To elaborate on that slightly, as a user of shields.io you can choose to share a token with us to help increase our rate limit. Here's how it works:
|
||||
|
||||
- Authorize our [OAuth Application](https://img.shields.io/github-auth).
|
||||
- This shares with us a GitHub token which has read-only access to public data. We only ask for the minimum permissions necessary. Authorizing the OAuth app doesn't allow us access to your private data or allow us to perform any actions on your behalf.
|
||||
- Your token is added to a pool of tokens shared by other users like you.
|
||||
- When we need to make a request to the GitHub API, we pick one of the tokens from our pool. We only make a handful of requests with each token before picking another from the pool.
|
||||
- If you ever decide you would not like to continue sharing a token with us, you can revoke the Shields.io OAuth app at https://github.com/settings/applications. You can do this at any time. This will de-activate the token you have shared with us and we'll remove it from the pool.
|
||||
|
||||
This method allows us (with your help) to make hundreds of thousands of request per hour to the GitHub API. Because we have thousands of tokens in the pool and we only make a few requests with each one before picking another token from the pool, most users don't notice any meaningful impact on their available rate limit as a result of authorizing our app.
|
||||
81
frontend/blog/2024-12-27-simpleicons14.md
Normal file
81
frontend/blog/2024-12-27-simpleicons14.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
slug: simple-icons-14
|
||||
title: Simple Icons 14
|
||||
authors:
|
||||
name: jNullj
|
||||
title: Shields.io Core Team
|
||||
url: https://github.com/jNullj
|
||||
image_url: https://avatars.githubusercontent.com/u/15849761
|
||||
tags: []
|
||||
---
|
||||
|
||||
Logos on Shields.io are provided by SimpleIcons. We've recently upgraded to SimpleIcons 14. This release removes 53 icons and renames 6:
|
||||
|
||||
Renames:
|
||||
|
||||
- D3.js to D3
|
||||
- Tencent QQ to QQ
|
||||
- T-Mobile to Deutsche Telekom
|
||||
- Nuxt.js to Nuxt
|
||||
- smash.gg start.gg
|
||||
- Tutanota to Tuta
|
||||
|
||||
Removals:
|
||||
|
||||
- Adobe
|
||||
- Adobe Acrobat Reader
|
||||
- Adobe After Effects
|
||||
- Adobe Audition
|
||||
- Adobe Creative Cloud
|
||||
- Adobe Dreamweaver
|
||||
- Adobe Fonts
|
||||
- Adobe Illustrator
|
||||
- Adobe InDesign
|
||||
- Adobe Lightroom
|
||||
- Adobe Lightroom Classic
|
||||
- Adobe Photoshop
|
||||
- Adobe Premiere Pro
|
||||
- Adobe XD
|
||||
- ASKfm
|
||||
- Caffeine
|
||||
- CKEditor 4
|
||||
- Cliqz
|
||||
- Coil
|
||||
- del.icio.us
|
||||
- El Jueves
|
||||
- Ello
|
||||
- FeatHub
|
||||
- Fluxus
|
||||
- Foursquare City Guide
|
||||
- Funimation
|
||||
- Game & Watch
|
||||
- Géant
|
||||
- Katacoda
|
||||
- LinkedIn
|
||||
- Magento
|
||||
- Marketo
|
||||
- Microgenetics
|
||||
- Nintendo
|
||||
- Nintendo 3DS
|
||||
- Nintendo DS
|
||||
- Nintendo GameCube
|
||||
- Nintendo Switch
|
||||
- Oracle
|
||||
- Pokémon
|
||||
- RadioPublic
|
||||
- Realm
|
||||
- Revue
|
||||
- Skyrock
|
||||
- Spinrilla
|
||||
- StackPath
|
||||
- Stitcher
|
||||
- Studyverse
|
||||
- Tableau
|
||||
- Uptobox
|
||||
- Wii
|
||||
- Wii U
|
||||
- Zerply
|
||||
|
||||
More detail can be found in the [release notes](https://github.com/simple-icons/simple-icons/releases/tag/14.0.0)
|
||||
|
||||
Please remember that we are just consumers of SimpleIcons. Decisions about changes and removals are made by the [SimpleIcons](https://github.com/simple-icons/simple-icons) project.
|
||||
@@ -123,6 +123,15 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Policy',
|
||||
items: [
|
||||
{
|
||||
label: 'Privacy Policy',
|
||||
href: '/privacy',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Shields.io. Built with Docusaurus.`,
|
||||
},
|
||||
|
||||
50
frontend/src/pages/privacy.md
Normal file
50
frontend/src/pages/privacy.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Privacy Policy
|
||||
|
||||
Shields.io is non-tracking and privacy-respecting. This Privacy Policy explains how we handle your data in compliance with the General Data Protection Regulation (GDPR).
|
||||
|
||||
## 1. Hosting and Service Providers
|
||||
|
||||
We use [fly.io](https://fly.io) for hosting and [CloudFlare](https://www.cloudflare.com) for DNS and CDN services. These third-party providers process requests to deliver and secure our website. Please refer to their privacy policies for more information:
|
||||
|
||||
- https://fly.io/legal/privacy-policy/
|
||||
- https://www.cloudflare.com/en-gb/privacypolicy/
|
||||
|
||||
## 2. Cookies
|
||||
|
||||
We do not use any cookies on our website.
|
||||
|
||||
## 3. Logs and Data Collection
|
||||
|
||||
We do not store any logs of your visits, requests, or other activities on our site.
|
||||
|
||||
## 4. Error Reporting
|
||||
|
||||
If a request fails, we send an error report to [Sentry](https://sentry.io/), our error-tracking service.
|
||||
These reports contain technical data about the error but do not include any personally identifiable information (PII), such as your IP address. For details on Sentry's data processing, refer to their privacy policy:
|
||||
|
||||
- https://sentry.io/privacy/
|
||||
|
||||
## 5. GitHub OAuth App
|
||||
|
||||
Users may optionally authorize our [GitHub OAuth app](https://img.shields.io/github-auth).
|
||||
|
||||
Authorizing our app shares with us a GitHub token which has read-only access to public data. We only ask for the minimum permissions necessary. Authorizing the OAuth app doesn't allow us access to your private data or allow us to perform any actions on your behalf.
|
||||
|
||||
The only information we store is the **GitHub token** and the **timestamp** when you authorized the app.
|
||||
|
||||
- The GitHub token is used solely to increase the rate limit for accessing the GitHub API.
|
||||
- The signup timestamp is stored for internal record-keeping purposes.
|
||||
|
||||
We don't collect or store any other information like your username or email address.
|
||||
|
||||
If you decide you would not like to continue sharing a token with us, you can revoke the Shields.io OAuth app at https://github.com/settings/applications. You can do this at any time. This will de-activate the token you have shared with us and we'll remove it from our token pool.
|
||||
|
||||
## 6. Your Rights
|
||||
|
||||
Under the GDPR, users have rights concerning their personal data, including access, correction, deletion, and objection to processing.
|
||||
|
||||
Since we process minimal data, these rights are not relevant to most users of the service.
|
||||
|
||||
## 7. Contact Us
|
||||
|
||||
If you have questions about this Privacy Policy or our data practices, you can contact us at team at shields.io
|
||||
@@ -261,7 +261,6 @@ function Curl({ postman, codeSamples }) {
|
||||
{tokens.map((line, i) => (
|
||||
// this <span> does have a key but eslint fails
|
||||
// to detect it because it is an arg to getLineProps()
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<span
|
||||
{...getLineProps({
|
||||
line,
|
||||
@@ -276,7 +275,6 @@ function Curl({ postman, codeSamples }) {
|
||||
return (
|
||||
// this <span> does have a key but eslint fails
|
||||
// to detect it because it is an arg to getLineProps()
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<span
|
||||
{...getTokenProps({
|
||||
token,
|
||||
|
||||
@@ -93,10 +93,13 @@ function getSimpleIcon({ name, color, style, size }) {
|
||||
if (size === 'auto') {
|
||||
const { width: iconWidth, height: iconHeight } = getIconSize(key)
|
||||
|
||||
if (iconWidth > iconHeight) {
|
||||
if (iconWidth !== iconHeight) {
|
||||
const path = resetIconPosition(simpleIcons[key].path)
|
||||
iconSvg = iconSvg
|
||||
.replace('viewBox="0 0 24 24"', `viewBox="0 0 24 ${iconHeight}"`)
|
||||
.replace(
|
||||
'viewBox="0 0 24 24"',
|
||||
`viewBox="0 0 ${iconWidth} ${iconHeight}"`,
|
||||
)
|
||||
.replace(/<path d=".*"\/>/, `<path d="${path}"/>`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined
|
||||
|
||||
exports.up = pgm => {
|
||||
|
||||
4141
package-lock.json
generated
4141
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
100
package.json
100
package.json
@@ -21,15 +21,16 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@renovatebot/pep440": "^3.0.20",
|
||||
"@renovatebot/ruby-semver": "^3.0.23",
|
||||
"@sentry/node": "^8.36.0",
|
||||
"@renovatebot/pep440": "^4.0.1",
|
||||
"@renovatebot/ruby-semver": "^4.0.0",
|
||||
"@sentry/node": "^8.47.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"@xmldom/xmldom": "0.9.5",
|
||||
"@xmldom/xmldom": "0.9.6",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"byte-size": "^9.0.1",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^8.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"chalk": "^5.4.1",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.12",
|
||||
@@ -38,33 +39,32 @@
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"fast-xml-parser": "^4.5.1",
|
||||
"glob": "^11.0.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.3",
|
||||
"graphql": "16.9.0",
|
||||
"got": "^14.4.5",
|
||||
"graphql": "16.10.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"joi": "17.13.3",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^10.1.0",
|
||||
"jsonpath-plus": "^10.2.0",
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"matcher": "^5.0.0",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^7.7.1",
|
||||
"node-pg-migrate": "^7.8.0",
|
||||
"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.0",
|
||||
"qs": "^6.13.1",
|
||||
"query-string": "^9.1.1",
|
||||
"semver": "~7.6.3",
|
||||
"simple-icons": "13.15.0",
|
||||
"smol-toml": "1.3.0",
|
||||
"simple-icons": "14.0.0",
|
||||
"smol-toml": "1.3.1",
|
||||
"svg-path-bbox": "^2.1.0",
|
||||
"svgpath": "^2.6.0",
|
||||
"webextension-store-meta": "^1.2.4",
|
||||
@@ -82,12 +82,12 @@
|
||||
"coverage:report:generate": "c8 report",
|
||||
"coverage:report:open": "open-cli coverage/lcov-report/index.html",
|
||||
"coverage:report": "run-s --silent coverage:report:generate coverage:report:open",
|
||||
"lint": "eslint \"**/*.@(cjs|js|ts|tsx)\"",
|
||||
"prettier": "prettier --write \"**/*.@(cjs|js|ts|tsx|md|json|yml)\"",
|
||||
"prettier:check": "prettier --check \"**/*.@(cjs|js|ts|tsx|md|json|yml)\"",
|
||||
"lint": "eslint \"**/*.@(cjs|mjs|js|ts)\"",
|
||||
"prettier": "prettier --write \"**/*.@(cjs|mjs|js|ts|md|json|yml)\"",
|
||||
"prettier:check": "prettier --check \"**/*.@(cjs|mjs|js|ts|md|json|yml)\"",
|
||||
"danger": "danger",
|
||||
"test:e2e": "cypress run",
|
||||
"test:core": "cross-env NODE_CONFIG_ENV=test mocha \"core/**/*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"test:core": "cross-env TZ='UTC' NODE_CONFIG_ENV=test mocha \"core/**/*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"test:package": "mocha \"badge-maker/**/*.spec.js\"",
|
||||
"test:entrypoint": "cross-env NODE_CONFIG_ENV=test mocha entrypoint.spec.js",
|
||||
"test:integration": "cross-env NODE_CONFIG_ENV=test mocha \"core/**/*.integration.js\" \"services/**/*.integration.js\"",
|
||||
@@ -124,7 +124,7 @@
|
||||
"docusaurus:clear": "docusaurus clear frontend"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.@(js|ts|tsx)": [
|
||||
"**/*.@(cjs|mjs|js|ts)": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
@@ -148,60 +148,56 @@
|
||||
"devDependencies": {
|
||||
"@docusaurus/core": "^3.5.2",
|
||||
"@docusaurus/preset-classic": "^3.5.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.45.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.46.1",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@typescript-eslint/parser": "^8.12.2",
|
||||
"c8": "^10.1.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"c8": "^10.1.3",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.5.0",
|
||||
"chai-as-promised": "^8.0.0",
|
||||
"chai": "5.1.2",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
"chai-datetime": "^1.8.1",
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"cypress": "^13.15.1",
|
||||
"concurrently": "^9.1.1",
|
||||
"cypress": "^13.17.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^12.3.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"docusaurus-preset-openapi": "0.7.5",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-config-standard-jsx": "11.0.0",
|
||||
"eslint-config-standard-react": "13.0.0",
|
||||
"eslint-plugin-chai-friendly": "^1.0.1",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.4.3",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-sort-class-members": "^1.21.0",
|
||||
"eslint-plugin-chai-friendly": "1.0.1",
|
||||
"eslint-plugin-cypress": "4.1.0",
|
||||
"eslint-plugin-icedfrisby": "0.2.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsdoc": "50.6.1",
|
||||
"eslint-plugin-mocha": "10.5.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"eslint-plugin-sort-class-members": "1.21.0",
|
||||
"form-data": "^4.0.1",
|
||||
"globals": "15.14.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.1.0",
|
||||
"jsdoc": "^4.0.4",
|
||||
"lint-staged": "^15.2.10",
|
||||
"lint-staged": "^15.2.11",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.8.2",
|
||||
"mocha": "^11.0.1",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.5.5",
|
||||
"node-mocks-http": "^1.16.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"neostandard": "0.12.0",
|
||||
"nock": "13.5.6",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^8.0.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "3.3.3",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"prettier": "3.4.2",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
@@ -209,9 +205,9 @@
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"sinon": "^19.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-chai": "4.0.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "2.0.8",
|
||||
"start-server-and-test": "2.0.9",
|
||||
"tsd": "^0.31.2",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
floorCount as floorCountColor,
|
||||
age as ageColor,
|
||||
} from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { metric, formatDate } from '../text-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import {
|
||||
BaseJsonService,
|
||||
@@ -243,16 +241,10 @@ class AurLastModified extends BaseAurService {
|
||||
|
||||
static defaultBadgeData = { label: 'last modified' }
|
||||
|
||||
static render({ date }) {
|
||||
const color = ageColor(date)
|
||||
const message = formatDate(date)
|
||||
return { color, message }
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
const date = 1000 * parseInt(json.results[0].LastModified)
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { relativeUri } from '../validators.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -43,13 +42,6 @@ export default class BitbucketLastCommit extends BaseJsonService {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, path }) {
|
||||
// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get
|
||||
return this._requestJson({
|
||||
@@ -76,6 +68,6 @@ export default class BitbucketLastCommit extends BaseJsonService {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({ commitDate: commit.date })
|
||||
return renderDateBadge(commit.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParams } from '../index.js'
|
||||
import BaseChromeWebStoreService from './chrome-web-store-base.js'
|
||||
|
||||
@@ -31,11 +30,6 @@ export default class ChromeWebStoreLastUpdated extends BaseChromeWebStoreService
|
||||
throw new NotFound({ prettyMessage: 'not found' })
|
||||
}
|
||||
|
||||
const lastUpdatedDate = Date.parse(lastUpdated)
|
||||
|
||||
return {
|
||||
message: formatDate(lastUpdatedDate),
|
||||
color: age(lastUpdatedDate),
|
||||
}
|
||||
return renderDateBadge(lastUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
|
||||
70
services/coderabbit/coderabbit-pull-request.service.js
Normal file
70
services/coderabbit/coderabbit-pull-request.service.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
reviews: Joi.number().required(),
|
||||
}).required()
|
||||
|
||||
class CodeRabbitPullRequest extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
static route = {
|
||||
base: 'coderabbit',
|
||||
pattern: 'prs/:provider(github|bitbucket|gitlab)/:org/:repo',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/coderabbit/prs/{provider}/{org}/{repo}': {
|
||||
get: {
|
||||
summary: 'CodeRabbit Pull Request Reviews',
|
||||
description:
|
||||
'This badge pulls the number of PRs reviewed by [CodeRabbit](https://coderabbit.ai), AI code review tool',
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'provider',
|
||||
example: 'github',
|
||||
description: 'Version Control Provider',
|
||||
schema: { type: 'string', enum: this.getEnum('provider') },
|
||||
},
|
||||
{
|
||||
name: 'org',
|
||||
example: 'coderabbitai',
|
||||
description: 'Organization or User name',
|
||||
},
|
||||
{
|
||||
name: 'repo',
|
||||
example: 'ast-grep-essentials',
|
||||
description: 'Repository name',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'coderabbit reviews',
|
||||
}
|
||||
|
||||
static render({ reviews }) {
|
||||
return {
|
||||
message: `${reviews}`,
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ provider, org, repo }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://api.coderabbit.ai/stats/${provider}/${org}/${repo}`,
|
||||
httpErrors: {
|
||||
400: 'provider or repo not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ provider, org, repo }) {
|
||||
const data = await this.fetch({ provider, org, repo })
|
||||
return this.constructor.render(data)
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeRabbitPullRequest
|
||||
25
services/coderabbit/coderabbit-pull-request.tester.js
Normal file
25
services/coderabbit/coderabbit-pull-request.tester.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('live CodeRabbitPullRequest')
|
||||
.get('/prs/github/coderabbitai/ast-grep-essentials.json')
|
||||
.expectBadge({
|
||||
label: 'coderabbit reviews',
|
||||
message: Joi.number().min(0),
|
||||
})
|
||||
|
||||
t.create('live CodeRabbitPullRequest nonexistent org')
|
||||
.get('/prs/github/not-valid/not-found.json')
|
||||
.expectBadge({
|
||||
label: 'coderabbit reviews',
|
||||
message: 'provider or repo not found',
|
||||
})
|
||||
|
||||
t.create('live CodeRabbitPullRequest invalid repo')
|
||||
.get('/prs/github/coderabbitai/invalid-repo-name.json')
|
||||
.expectBadge({
|
||||
label: 'coderabbit reviews',
|
||||
message: 'provider or repo not found',
|
||||
})
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import pep440 from '@renovatebot/pep440'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge based on version.
|
||||
@@ -175,24 +174,7 @@ function colorScale(steps, colors, reversed) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge according to the age.
|
||||
* Age is calculated as days elapsed till current date.
|
||||
* The color varies from bright green to red as the age increases
|
||||
* or the other way around if `reverse` is given `true`.
|
||||
*
|
||||
* @param {string} date Date string
|
||||
* @param {boolean} reversed Reverse the color scale a.k.a. the older, the better
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function age(date, reversed = false) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, !reversed)
|
||||
const daysElapsed = dayjs().diff(dayjs(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
export {
|
||||
age,
|
||||
colorScale,
|
||||
coveragePercentage,
|
||||
downloadCount,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { forCases, given, test } from 'sazerac'
|
||||
import {
|
||||
age,
|
||||
colorScale,
|
||||
coveragePercentage,
|
||||
letterScore,
|
||||
@@ -53,46 +52,6 @@ describe('Color formatters', function () {
|
||||
given('Z').expect('red')
|
||||
})
|
||||
|
||||
const monthsAgo = months => {
|
||||
const result = new Date()
|
||||
// This looks wack but it works.
|
||||
result.setMonth(result.getMonth() - months)
|
||||
return result
|
||||
}
|
||||
test(age, () => {
|
||||
given(Date.now())
|
||||
.describe('when given the current timestamp')
|
||||
.expect('brightgreen')
|
||||
given(new Date())
|
||||
.describe('when given the current Date')
|
||||
.expect('brightgreen')
|
||||
given(new Date(2001, 1, 1))
|
||||
.describe('when given a Date many years ago')
|
||||
.expect('red')
|
||||
given(monthsAgo(2))
|
||||
.describe('when given a Date two months ago')
|
||||
.expect('yellowgreen')
|
||||
given(monthsAgo(15))
|
||||
.describe('when given a Date 15 months ago')
|
||||
.expect('orange')
|
||||
// --- reversed --- //
|
||||
given(Date.now(), true)
|
||||
.describe('when given the current timestamp and reversed')
|
||||
.expect('red')
|
||||
given(new Date(), true)
|
||||
.describe('when given the current Date and reversed')
|
||||
.expect('red')
|
||||
given(new Date(2001, 1, 1), true)
|
||||
.describe('when given a Date many years ago and reversed')
|
||||
.expect('brightgreen')
|
||||
given(monthsAgo(2), true)
|
||||
.describe('when given a Date two months ago and reversed')
|
||||
.expect('yellow')
|
||||
given(monthsAgo(15), true)
|
||||
.describe('when given a Date 15 months ago and reversed')
|
||||
.expect('green')
|
||||
})
|
||||
|
||||
test(version, () => {
|
||||
forCases([given('1.0'), given(9), given(1.0)]).expect('blue')
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
|
||||
const versionSchema = Joi.object({
|
||||
downloads: nonNegativeInteger,
|
||||
// Crate size is not available for all versions.
|
||||
crate_size: nonNegativeInteger.allow(null),
|
||||
crate_size: nonNegativeInteger,
|
||||
num: Joi.string().required(),
|
||||
license: Joi.string().required().allow(null),
|
||||
rust_version: Joi.string().allow(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { InvalidResponse, pathParams } from '../index.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { renderSizeBadge } from '../size.js'
|
||||
import { BaseCratesService, description } from './crates-base.js'
|
||||
|
||||
export default class CratesSize extends BaseCratesService {
|
||||
@@ -38,22 +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
|
||||
|
||||
if (size == null) {
|
||||
throw new InvalidResponse({ prettyMessage: 'unknown' })
|
||||
}
|
||||
|
||||
return this.render({ size })
|
||||
return renderSizeBadge(size, 'iec')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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' })
|
||||
|
||||
t.create('size (with version where version doesnt have size)')
|
||||
.get('/tokio/0.1.6.json')
|
||||
.expectBadge({ label: 'crates.io', message: 'unknown' })
|
||||
.expectBadge({ label: 'size', message: '708 KiB' })
|
||||
|
||||
t.create('size (not found)')
|
||||
.get('/not-a-crate.json')
|
||||
|
||||
108
services/date.js
Normal file
108
services/date.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Commonly-used functions for rendering badges containing a date
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import calendar from 'dayjs/plugin/calendar.js'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import { colorScale } from './color-formatters.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
|
||||
dayjs.extend(calendar)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
/**
|
||||
* Parse and validate a string date into a dayjs object. Use this helper
|
||||
* in preference to invoking dayjs directly when parsing a date from string.
|
||||
*
|
||||
* @param {...any} args - Variadic: Arguments to pass through to dayjs
|
||||
* @returns {dayjs} - Parsed object
|
||||
* @throws {InvalidResponse} - Error if validation fails
|
||||
* @see https://day.js.org/docs/en/parse/string
|
||||
* @see https://day.js.org/docs/en/parse/string-format
|
||||
* @see https://day.js.org/docs/en/parse/is-valid
|
||||
* @example
|
||||
* parseDate('2024-01-01')
|
||||
* parseDate('31/01/2024', 'DD/MM/YYYY')
|
||||
* parseDate('2018 Enero 15', 'YYYY MMMM DD', 'es')
|
||||
*/
|
||||
function parseDate(...args) {
|
||||
let date
|
||||
if (args.length >= 2) {
|
||||
// always use strict mode if format arg is supplied
|
||||
date = dayjs(...args, true)
|
||||
} else {
|
||||
date = dayjs(...args)
|
||||
}
|
||||
if (!date.isValid()) {
|
||||
throw new InvalidResponse({ prettyMessage: 'invalid date' })
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted date string without the year based on the value of input date param d.
|
||||
*
|
||||
* @param {Date | string | number | dayjs } d JS Date object, string, unix timestamp or dayjs object
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
function formatDate(d) {
|
||||
const date = parseDate(d)
|
||||
const dateString = date.calendar(null, {
|
||||
lastDay: '[yesterday]',
|
||||
sameDay: '[today]',
|
||||
lastWeek: '[last] dddd',
|
||||
sameElse: 'MMMM YYYY',
|
||||
})
|
||||
// Trim current year from date string
|
||||
return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge according to the age.
|
||||
* Age is calculated as days elapsed till current date.
|
||||
* The color varies from bright green to red as the age increases
|
||||
* or the other way around if `reverse` is given `true`.
|
||||
*
|
||||
* @param {Date | string | number | dayjs } date JS Date object, string, unix timestamp or dayjs object
|
||||
* @param {boolean} reversed Reverse the color scale (the older, the better)
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function age(date, reversed = false) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, !reversed)
|
||||
const daysElapsed = dayjs().diff(parseDate(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a badge object that displays a date
|
||||
*
|
||||
* @param {Date | string | number | dayjs } date JS Date object, string, unix timestamp or dayjs object
|
||||
* @param {boolean} reversed Reverse the color scale (the older, the better)
|
||||
* @returns {object} A badge object that has two properties: message, and color
|
||||
*/
|
||||
function renderDateBadge(date, reversed = false) {
|
||||
const d = parseDate(date)
|
||||
const color = age(d, reversed)
|
||||
const message = formatDate(d)
|
||||
return { message, color }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a relative date from the input timestamp.
|
||||
* For example, day after tomorrow's timestamp will return 'in 2 days'.
|
||||
*
|
||||
* @param {number | string} timestamp - Unix timestamp
|
||||
* @returns {string} Relative date from the unix timestamp
|
||||
*/
|
||||
function formatRelativeDate(timestamp) {
|
||||
const parsedDate = dayjs.unix(parseInt(timestamp, 10))
|
||||
if (!parsedDate.isValid()) {
|
||||
return 'invalid date'
|
||||
}
|
||||
return dayjs().to(parsedDate).toLowerCase()
|
||||
}
|
||||
|
||||
export { parseDate, renderDateBadge, formatDate, formatRelativeDate, age }
|
||||
132
services/date.spec.js
Normal file
132
services/date.spec.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import sinon from 'sinon'
|
||||
import { parseDate, formatDate, formatRelativeDate, age } from './date.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
|
||||
describe('parseDate', function () {
|
||||
it('parses valid inputs', function () {
|
||||
expect(parseDate('2024-01-01').valueOf()).to.equal(
|
||||
new Date('2024-01-01').valueOf(),
|
||||
)
|
||||
expect(parseDate('Jan 01 01:00:00 2024 GMT').valueOf()).to.equal(
|
||||
new Date('2024-01-01T01:00:00.000Z').valueOf(),
|
||||
)
|
||||
expect(parseDate('31/01/2024', 'DD/MM/YYYY').valueOf()).to.equal(
|
||||
new Date('2024-01-31T00:00:00.000Z').valueOf(),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws when given invalid inputs', function () {
|
||||
// not a date
|
||||
expect(() => parseDate('foo')).to.throw(InvalidResponse)
|
||||
expect(() => parseDate([])).to.throw(InvalidResponse)
|
||||
expect(() => parseDate(null)).to.throw(InvalidResponse)
|
||||
|
||||
// invalid dates (only works with format string)
|
||||
expect(() => parseDate('2024-02-31', 'YYYY-MM-DD')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
expect(() => parseDate('2024-12-32', 'YYYY-MM-DD')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
|
||||
// non-standard format with no format string
|
||||
expect(() => parseDate('31/01/2024')).to.throw(InvalidResponse)
|
||||
|
||||
// parse format doesn't match date
|
||||
expect(() => parseDate('2024-01-01', 'YYYYMMDDHHmmss')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
})
|
||||
|
||||
test(formatDate, () => {
|
||||
given(1465513200000)
|
||||
.describe('when given a timestamp in june 2016')
|
||||
.expect('june 2016')
|
||||
})
|
||||
|
||||
context('in october', function () {
|
||||
let clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers(new Date(2017, 9, 15).getTime())
|
||||
})
|
||||
afterEach(function () {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
test(formatDate, () => {
|
||||
given(new Date(2017, 0, 1).getTime())
|
||||
.describe('when given the beginning of this year')
|
||||
.expect('january')
|
||||
})
|
||||
})
|
||||
|
||||
context('in october', function () {
|
||||
let clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers(new Date(2018, 9, 29).getTime())
|
||||
})
|
||||
afterEach(function () {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(new Date(2018, 9, 31).getTime() / 1000)
|
||||
.describe('when given the end of october')
|
||||
.expect('in 2 days')
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(new Date(2018, 9, 1).getTime() / 1000)
|
||||
.describe('when given the beginning of october')
|
||||
.expect('a month ago')
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(9999999999999)
|
||||
.describe('when given invalid date')
|
||||
.expect('invalid date')
|
||||
})
|
||||
})
|
||||
|
||||
const monthsAgo = months => {
|
||||
const result = new Date()
|
||||
// This looks wack but it works.
|
||||
result.setMonth(result.getMonth() - months)
|
||||
return result
|
||||
}
|
||||
test(age, () => {
|
||||
given(Date.now())
|
||||
.describe('when given the current timestamp')
|
||||
.expect('brightgreen')
|
||||
given(new Date())
|
||||
.describe('when given the current Date')
|
||||
.expect('brightgreen')
|
||||
given(new Date(2001, 1, 1))
|
||||
.describe('when given a Date many years ago')
|
||||
.expect('red')
|
||||
given(monthsAgo(2))
|
||||
.describe('when given a Date two months ago')
|
||||
.expect('yellowgreen')
|
||||
given(monthsAgo(15))
|
||||
.describe('when given a Date 15 months ago')
|
||||
.expect('orange')
|
||||
// --- reversed --- //
|
||||
given(Date.now(), true)
|
||||
.describe('when given the current timestamp and reversed')
|
||||
.expect('red')
|
||||
given(new Date(), true)
|
||||
.describe('when given the current Date and reversed')
|
||||
.expect('red')
|
||||
given(new Date(2001, 1, 1), true)
|
||||
.describe('when given a Date many years ago and reversed')
|
||||
.expect('brightgreen')
|
||||
given(monthsAgo(2), true)
|
||||
.describe('when given a Date two months ago and reversed')
|
||||
.expect('yellow')
|
||||
given(monthsAgo(15), true)
|
||||
.describe('when given a Date 15 months ago and reversed')
|
||||
.expect('green')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatRelativeDate } from '../text-formatters.js'
|
||||
import { formatRelativeDate } from '../date.js'
|
||||
import { BaseService, pathParams } from '../index.js'
|
||||
|
||||
const description = `
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -3,10 +3,10 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('docker version (valid, library)')
|
||||
.get('/_/memcached.json')
|
||||
.get('/docker/example-voting-app-vote.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
message: isSemver,
|
||||
message: 'latest',
|
||||
})
|
||||
|
||||
t.create('docker version (valid, library with tag)')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import chai from 'chai'
|
||||
import { expect, use } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import jsonPath from './json-path.js'
|
||||
const { expect } = chai
|
||||
chai.use(chaiAsPromised)
|
||||
use(chaiAsPromised)
|
||||
|
||||
describe('JSON Path service factory', function () {
|
||||
describe('fetch()', function () {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import EclipseMarketplaceBase from './eclipse-marketplace-base.js'
|
||||
|
||||
@@ -34,19 +33,12 @@ export default class EclipseMarketplaceUpdate extends EclipseMarketplaceBase {
|
||||
|
||||
static defaultBadgeData = { label: 'updated' }
|
||||
|
||||
static render({ date }) {
|
||||
return {
|
||||
message: formatDate(date),
|
||||
color: ageColor(date),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ name }) {
|
||||
const { marketplace } = await this.fetch({
|
||||
name,
|
||||
schema: updateResponseSchema,
|
||||
})
|
||||
const date = 1000 * parseInt(marketplace.node.changed)
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const endpointSchema = Joi.object({
|
||||
namedLogo: Joi.string(),
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoSize: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
style: Joi.string(),
|
||||
cacheSeconds: Joi.number().integer().min(0),
|
||||
|
||||
@@ -93,6 +93,14 @@ The endpoint badge takes a single required query param: <code>url</code>, which
|
||||
the query string. Only works for simple-icons logos.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoSize</code></td>
|
||||
<td>
|
||||
Default: none. Make icons adaptively resize by setting <code>auto</code>.
|
||||
Useful for some wider logos like <code>amd</code> and <code>amg</code>.
|
||||
Supported for simple-icons logos only.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoWidth</code></td>
|
||||
<td>
|
||||
@@ -147,6 +155,7 @@ export default class Endpoint extends BaseJsonService {
|
||||
namedLogo,
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoSize,
|
||||
logoWidth,
|
||||
style,
|
||||
cacheSeconds,
|
||||
@@ -160,6 +169,7 @@ export default class Endpoint extends BaseJsonService {
|
||||
namedLogo,
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoSize,
|
||||
logoWidth,
|
||||
style,
|
||||
// don't allow the user to set cacheSeconds any shorter than this._cacheLength
|
||||
|
||||
@@ -82,6 +82,22 @@ t.create('named logo with color')
|
||||
expect(body).to.include(getSimpleIcon({ name: 'github', color: 'blue' }))
|
||||
})
|
||||
|
||||
t.create('named logo with size')
|
||||
.get('.svg?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/').get('/badge').reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
namedLogo: 'github',
|
||||
logoSize: 'auto',
|
||||
}),
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getSimpleIcon({ name: 'github', size: 'auto' }))
|
||||
})
|
||||
|
||||
const logoSvg = Buffer.from(
|
||||
getSimpleIcon({ name: 'npm' }).replace('data:image/svg+xml;base64,', ''),
|
||||
'base64',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
@@ -131,18 +130,9 @@ class FactorioModPortalLastUpdated extends BaseFactorioModPortalService {
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
static render({ lastUpdated }) {
|
||||
return {
|
||||
message: formatDate(lastUpdated),
|
||||
color: age(lastUpdated),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ modName }) {
|
||||
const resp = await this.fetch({ modName })
|
||||
return this.constructor.render({
|
||||
lastUpdated: resp.latest_release.released_at,
|
||||
})
|
||||
return renderDateBadge(resp.latest_release.released_at)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import BaseGalaxyToolshedService from './galaxytoolshed-base.js'
|
||||
|
||||
export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService {
|
||||
@@ -29,11 +29,6 @@ export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'created date',
|
||||
color: 'blue',
|
||||
}
|
||||
|
||||
static render({ date }) {
|
||||
return { message: formatDate(date) }
|
||||
}
|
||||
|
||||
async handle({ repository, owner }) {
|
||||
@@ -42,6 +37,6 @@ export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService
|
||||
owner,
|
||||
})
|
||||
const { create_time: date } = response[0]
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { optionalUrl, relativeUri } from '../validators.js'
|
||||
import GiteaBase from './gitea-base.js'
|
||||
import { description, httpErrorsFor } from './gitea-helper.js'
|
||||
@@ -114,13 +113,6 @@ export default class GiteaLastCommit extends GiteaBase {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, baseUrl, path }) {
|
||||
// https://gitea.com/api/swagger#/repository
|
||||
return super.fetch({
|
||||
@@ -146,8 +138,6 @@ export default class GiteaLastCommit extends GiteaBase {
|
||||
baseUrl,
|
||||
path,
|
||||
})
|
||||
return this.constructor.render({
|
||||
commitDate: body[0].commit[displayTimestamp].date,
|
||||
})
|
||||
return renderDateBadge(body[0].commit[displayTimestamp].date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,10 +114,11 @@ describe('Github token acceptor', function () {
|
||||
const res = await got.post(`${baseUrl}/github-auth/done`, {
|
||||
body: form,
|
||||
})
|
||||
expect(res.body).to.startWith(
|
||||
'<p>Shields.io has received your app-specific GitHub user token.',
|
||||
)
|
||||
|
||||
expect(
|
||||
res.body.startsWith(
|
||||
'<p>Shields.io has received your app-specific GitHub user token.',
|
||||
),
|
||||
).to.be.true
|
||||
expect(onTokenAccepted).to.have.been.calledWith(fakeAccessToken)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../../index.js'
|
||||
import { formatDate } from '../../text-formatters.js'
|
||||
import { age as ageColor } from '../../color-formatters.js'
|
||||
import { renderDateBadge } from '../../date.js'
|
||||
import { GithubAuthV3Service } from '../github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from '../github-helpers.js'
|
||||
|
||||
@@ -27,13 +26,6 @@ export default class GistLastCommit extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ gistId }) {
|
||||
return this._requestJson({
|
||||
url: `/gists/${gistId}`,
|
||||
@@ -44,6 +36,6 @@ export default class GistLastCommit extends GithubAuthV3Service {
|
||||
|
||||
async handle({ gistId }) {
|
||||
const { updated_at: commitDate } = await this.fetch({ gistId })
|
||||
return this.constructor.render({ commitDate })
|
||||
return renderDateBadge(commitDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
|
||||
@@ -34,14 +32,6 @@ export default class GithubCreatedAt extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'created at' }
|
||||
|
||||
static render({ createdAt }) {
|
||||
const date = dayjs(createdAt)
|
||||
return {
|
||||
message: formatDate(date),
|
||||
color: age(date, true),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const { created_at: createdAt } = await this._requestJson({
|
||||
schema,
|
||||
@@ -49,6 +39,6 @@ export default class GithubCreatedAt extends GithubAuthV3Service {
|
||||
httpErrors: httpErrorsFor('repo not found'),
|
||||
})
|
||||
|
||||
return this.constructor.render({ createdAt })
|
||||
return renderDateBadge(createdAt, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import gql from 'graphql-tag'
|
||||
import Joi from 'joi'
|
||||
import dayjs from 'dayjs'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { parseDate } from '../date.js'
|
||||
import { metric, maybePluralize } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
@@ -97,7 +98,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
// The global cutoff time is 11/1 noon UTC.
|
||||
// https://github.com/badges/shields/pull/4109#discussion_r330782093
|
||||
// We want to show "1 day left" on the last day so we add 1.
|
||||
daysLeft = dayjs(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
|
||||
daysLeft = parseDate(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
|
||||
}
|
||||
if (daysLeft < 0) {
|
||||
return {
|
||||
@@ -181,7 +182,10 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
}
|
||||
|
||||
static getCalendarPosition(year) {
|
||||
const daysToStart = dayjs(`${year}-10-01 00:00:00 Z`).diff(dayjs(), 'days')
|
||||
const daysToStart = parseDate(`${year}-10-01 00:00:00 Z`).diff(
|
||||
dayjs(),
|
||||
'days',
|
||||
)
|
||||
const isBefore = daysToStart > 0
|
||||
return { daysToStart, isBefore }
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { colorScale } from '../color-formatters.js'
|
||||
import { InvalidResponse, NotFound } from '../index.js'
|
||||
|
||||
const documentation = `
|
||||
If your GitHub badge errors, it might be because you hit GitHub's rate limits.
|
||||
You can increase Shields.io's rate limit by
|
||||
[adding the Shields GitHub application](https://img.shields.io/github-auth)
|
||||
using your GitHub account.
|
||||
You can help increase Shields.io's rate limit by
|
||||
[authorizing the Shields.io GitHub application](https://img.shields.io/github-auth).
|
||||
Read more about [how it works](/blog/token-pool).
|
||||
`
|
||||
|
||||
function issueStateColor(s) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { formatDate, metric } from '../text-formatters.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { InvalidResponse, pathParams } from '../index.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import {
|
||||
@@ -133,11 +133,13 @@ const ageUpdateMap = {
|
||||
}).required(),
|
||||
transform: ({ json, property }) =>
|
||||
property === 'age' ? json.created_at : json.updated_at,
|
||||
render: ({ property, value }) => ({
|
||||
color: age(value),
|
||||
label: property === 'age' ? 'created' : 'updated',
|
||||
message: formatDate(value),
|
||||
}),
|
||||
render: ({ property, value }) => {
|
||||
const label = property === 'age' ? 'created' : 'updated'
|
||||
return {
|
||||
...renderDateBadge(value),
|
||||
label,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const milestoneMap = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate, metric } from '../text-formatters.js'
|
||||
import { age, formatDate } from '../date.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import GithubIssueDetail from './github-issue-detail.service.js'
|
||||
import { issueStateColor, commentsColor } from './github-helpers.js'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { relativeUri } from '../validators.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
@@ -88,13 +87,6 @@ export default class GithubLastCommit extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, path }) {
|
||||
return this._requestJson({
|
||||
url: `/repos/${user}/${repo}/commits`,
|
||||
@@ -111,8 +103,6 @@ export default class GithubLastCommit extends GithubAuthV3Service {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({
|
||||
commitDate: commit[displayTimestamp].date,
|
||||
})
|
||||
return renderDateBadge(commit[displayTimestamp].date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ export const t = new ServiceTester({
|
||||
t.create('Manifest version')
|
||||
.get('/v/sindresorhus/show-all-github-issues.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
label: 'manifest',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('Manifest version (path)')
|
||||
.get('/v/RedSparr0w/IndieGala-Helper.json?filename=extension/manifest.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
label: 'manifest',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import {
|
||||
isCommitHash,
|
||||
isVPlusDottedVersionAtLeastOne,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
|
||||
// e.g. v19.3b0
|
||||
const isBlackVersion = Joi.string().regex(/^v\d+(\.\d+)*(.*)?$/)
|
||||
const isShortSha = Joi.string().regex(/[0-9a-f]{7}/)
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'GithubPipenv',
|
||||
@@ -82,10 +82,8 @@ t.create('Locked version of unknown dependency')
|
||||
})
|
||||
|
||||
t.create('Locked version of VCS dependency')
|
||||
.get(
|
||||
'/locked/dependency-version/thorn-oss/perception/dev/videoalignment.json',
|
||||
)
|
||||
.get('/locked/dependency-version/pypa/pipenv/dev/pypiserver.json')
|
||||
.expectBadge({
|
||||
label: 'videoalignment',
|
||||
message: isShortSha,
|
||||
label: 'pypiserver',
|
||||
message: isCommitHash,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
|
||||
@@ -63,14 +61,6 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'release date' }
|
||||
|
||||
static render({ date }) {
|
||||
const releaseDate = dayjs(date)
|
||||
return {
|
||||
message: formatDate(releaseDate),
|
||||
color: age(releaseDate),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ variant, user, repo }) {
|
||||
const url =
|
||||
variant === 'release-date'
|
||||
@@ -86,10 +76,8 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
|
||||
async handle({ variant, user, repo }, queryParams) {
|
||||
const body = await this.fetch({ variant, user, repo })
|
||||
if (Array.isArray(body)) {
|
||||
return this.constructor.render({
|
||||
date: body[0][queryParams.display_date],
|
||||
})
|
||||
return renderDateBadge(body[0][queryParams.display_date])
|
||||
}
|
||||
return this.constructor.render({ date: body[queryParams.display_date] })
|
||||
return renderDateBadge(body[queryParams.display_date])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { redirector, pathParam, queryParam } from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import {
|
||||
fetchLatestRelease,
|
||||
@@ -46,13 +45,6 @@ class GithubRelease extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'release' }
|
||||
|
||||
static render({ version, sort, isPrerelease }) {
|
||||
let color = 'blue'
|
||||
color = sort === 'semver' ? versionColor(version) : color
|
||||
color = isPrerelease ? 'orange' : color
|
||||
return { message: addv(version), color }
|
||||
}
|
||||
|
||||
static transform(latestRelease, display) {
|
||||
const { name, tag_name: tagName, prerelease: isPrerelease } = latestRelease
|
||||
if (display === 'tag') {
|
||||
@@ -72,9 +64,8 @@ class GithubRelease extends GithubAuthV3Service {
|
||||
latestRelease,
|
||||
queryParams.display_name,
|
||||
)
|
||||
return this.constructor.render({
|
||||
return renderVersionBadge({
|
||||
version,
|
||||
sort: queryParams.sort,
|
||||
isPrerelease,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../index.js'
|
||||
import { queryParams, redirector } from '../index.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
@@ -7,33 +7,35 @@ import { documentation } from './github-helpers.js'
|
||||
|
||||
const schema = Joi.object({ total_count: nonNegativeInteger }).required()
|
||||
|
||||
export default class GithubSearch extends GithubAuthV3Service {
|
||||
const queryParamSchema = Joi.object({
|
||||
query: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
const codeSearchDocs = `
|
||||
For a full list of available filters and allowed values,
|
||||
see GitHub's documentation on
|
||||
[Searching code](https://docs.github.com/en/search-github/github-code-search/understanding-github-code-search-syntax)`
|
||||
|
||||
class GitHubCodeSearch extends GithubAuthV3Service {
|
||||
static category = 'analysis'
|
||||
|
||||
static route = {
|
||||
base: 'github/search',
|
||||
pattern: ':user/:repo/:query+',
|
||||
base: 'github',
|
||||
pattern: 'search',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/github/search/{user}/{repo}/{query}': {
|
||||
'/github/search': {
|
||||
get: {
|
||||
summary: 'GitHub search hit counter',
|
||||
summary: 'GitHub code search count',
|
||||
description: documentation,
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'user',
|
||||
example: 'torvalds',
|
||||
},
|
||||
{
|
||||
name: 'repo',
|
||||
example: 'linux',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
example: 'goto',
|
||||
},
|
||||
),
|
||||
parameters: queryParams({
|
||||
name: 'query',
|
||||
description: codeSearchDocs,
|
||||
example: 'goto language:javascript NOT is:fork NOT is:archived',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -50,21 +52,35 @@ export default class GithubSearch extends GithubAuthV3Service {
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, repo, query }) {
|
||||
async handle(_routeParams, { query }) {
|
||||
const { total_count: totalCount } = await this._requestJson({
|
||||
url: '/search/code',
|
||||
options: {
|
||||
searchParams: {
|
||||
q: `${query} repo:${user}/${repo}`,
|
||||
q: query,
|
||||
},
|
||||
},
|
||||
schema,
|
||||
httpErrors: {
|
||||
401: 'auth required for search api',
|
||||
404: 'repo not found',
|
||||
422: 'repo not found',
|
||||
},
|
||||
})
|
||||
|
||||
return this.constructor.render({ query, totalCount })
|
||||
}
|
||||
}
|
||||
|
||||
const GitHubCodeSearchRedirect = redirector({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'github/search',
|
||||
pattern: ':user/:repo/:query+',
|
||||
},
|
||||
transformPath: () => '/github/search',
|
||||
transformQueryParams: ({ query, user, repo }) => ({
|
||||
query: `${query} repo:${user}/${repo}`,
|
||||
}),
|
||||
dateAdded: new Date('2024-11-29'),
|
||||
})
|
||||
|
||||
export { GitHubCodeSearch, GitHubCodeSearchRedirect }
|
||||
|
||||
@@ -3,9 +3,18 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('hit counter')
|
||||
.get('/badges/shields/async%20handle.json')
|
||||
.get('/search.json?query=async%20handle')
|
||||
.expectBadge({ label: 'async handle counter', message: isMetric })
|
||||
|
||||
t.create('hit counter for nonexistent repo')
|
||||
.get('/badges/puppets/async%20handle.json')
|
||||
.expectBadge({ label: 'async handle counter', message: '0' })
|
||||
t.create('hit counter, zero results')
|
||||
.get('/search.json?query=async%20handle%20repo%3Abadges%2Fpuppets')
|
||||
.expectBadge({
|
||||
label: 'async handle repo:badges/puppets counter',
|
||||
message: '0',
|
||||
})
|
||||
|
||||
t.create('legacy redirect')
|
||||
.get('/search/badges/shields/async%20handle.svg')
|
||||
.expectRedirect(
|
||||
'/github/search.svg?query=async%20handle%20repo%3Abadges%2Fshields',
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
import Joi from 'joi'
|
||||
import { matcher } from 'matcher'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { latest } from '../version.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { NotFound, redirector, pathParam } from '../index.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
import {
|
||||
@@ -55,13 +53,6 @@ class GithubTag extends GithubAuthV4Service {
|
||||
label: 'tag',
|
||||
}
|
||||
|
||||
static render({ version, sort }) {
|
||||
return {
|
||||
message: addv(version),
|
||||
color: sort === 'semver' ? versionColor(version) : 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
static getLimit({ sort, filter }) {
|
||||
if (!filter && sort === 'date') {
|
||||
return 1
|
||||
@@ -123,13 +114,12 @@ class GithubTag extends GithubAuthV4Service {
|
||||
const prettyMessage = filter ? 'no matching tags found' : 'no tags found'
|
||||
throw new NotFound({ prettyMessage })
|
||||
}
|
||||
return this.constructor.render({
|
||||
return renderVersionBadge({
|
||||
version: this.constructor.getLatestTag({
|
||||
tags,
|
||||
sort,
|
||||
includePrereleases,
|
||||
}),
|
||||
sort,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,6 @@ describe('GithubTag', function () {
|
||||
}).expect('1.2.0-beta')
|
||||
})
|
||||
|
||||
test(GithubTag.render, () => {
|
||||
given({ usingSemver: false, version: '1.2.3' }).expect({
|
||||
message: 'v1.2.3',
|
||||
color: 'blue',
|
||||
})
|
||||
given({ usingSemver: true, version: '2.0.0' }).expect({
|
||||
message: 'v2.0.0',
|
||||
color: 'blue',
|
||||
})
|
||||
})
|
||||
|
||||
test(GithubTag.getLimit, () => {
|
||||
given({ sort: 'date', filter: undefined }).expect(1)
|
||||
given({ sort: 'date', filter: '' }).expect(1)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isDecimalPercentage } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('top language')
|
||||
.get('/badges/shields.json')
|
||||
.expectBadge({
|
||||
label: 'javascript',
|
||||
message: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/),
|
||||
})
|
||||
t.create('top language').get('/badges/shields.json').expectBadge({
|
||||
label: 'javascript',
|
||||
message: isDecimalPercentage,
|
||||
})
|
||||
|
||||
t.create('top language (empty repo)')
|
||||
.get('/pyvesb/emptyrepo.json')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { optionalUrl, relativeUri } from '../validators.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
import { description, httpErrorsFor } from './gitlab-helper.js'
|
||||
@@ -66,13 +65,6 @@ export default class GitlabLastCommit extends GitLabBase {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl, ref, path }) {
|
||||
// https://docs.gitlab.com/ee/api/commits.html#list-repository-commits
|
||||
return super.fetch({
|
||||
@@ -94,6 +86,6 @@ export default class GitlabLastCommit extends GitLabBase {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({ commitDate: commit.committed_date })
|
||||
return renderDateBadge(commit.committed_date)
|
||||
}
|
||||
}
|
||||
|
||||
79
services/gitlab/gitlab-top-language.service.js
Normal file
79
services/gitlab/gitlab-top-language.service.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { InvalidResponse, pathParam, queryParam } from '../index.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
import { description, httpErrorsFor } from './gitlab-helper.js'
|
||||
|
||||
const schema = Joi.object()
|
||||
.pattern(
|
||||
Joi.string().required(),
|
||||
Joi.number().min(0).max(100).precision(2).required(),
|
||||
)
|
||||
.required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
gitlab_url: optionalUrl,
|
||||
}).required()
|
||||
|
||||
export default class GitlabTopLanguage extends GitLabBase {
|
||||
static category = 'analysis'
|
||||
|
||||
static route = {
|
||||
base: 'gitlab/languages',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/gitlab/languages/{project}': {
|
||||
get: {
|
||||
summary: 'GitLab Top Language',
|
||||
description,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'project',
|
||||
example: 'gitlab-org/gitlab',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'gitlab_url',
|
||||
example: 'https://gitlab.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'language' }
|
||||
|
||||
static render({ languageData }) {
|
||||
const topLanguage = Object.keys(languageData).reduce((a, b) =>
|
||||
languageData[a] > languageData[b] ? a : b,
|
||||
)
|
||||
return {
|
||||
label: topLanguage.toLowerCase(),
|
||||
message: `${languageData[topLanguage].toFixed(1)}%`,
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl }) {
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(project)}/languages`,
|
||||
httpErrors: httpErrorsFor('project not found'),
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
|
||||
const languageData = await this.fetch({
|
||||
project,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
if (Object.keys(languageData).length > 0) {
|
||||
return this.constructor.render({ languageData })
|
||||
} else {
|
||||
throw new InvalidResponse({ prettyMessage: 'no languages found' })
|
||||
}
|
||||
}
|
||||
}
|
||||
23
services/gitlab/gitlab-top-language.tester.js
Normal file
23
services/gitlab/gitlab-top-language.tester.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isDecimalPercentage } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Valid Repository').get('/wireshark/wireshark.json').expectBadge({
|
||||
label: 'c',
|
||||
message: isDecimalPercentage,
|
||||
})
|
||||
|
||||
t.create('Valid Blank Repo')
|
||||
.get('/KoruptTinker/gitlab-blank-repo.json')
|
||||
.expectBadge({
|
||||
label: 'language',
|
||||
message: 'no languages found',
|
||||
})
|
||||
|
||||
t.create('Invalid Repository')
|
||||
.get('/wireshark/invalidexample.json')
|
||||
.expectBadge({
|
||||
label: 'language',
|
||||
message: 'project not found',
|
||||
})
|
||||
@@ -10,9 +10,9 @@ t.create('known issue')
|
||||
.expectBadge({ label: 'kafka-2896', message: 'Resolved' })
|
||||
|
||||
t.create('no status color')
|
||||
.get('/foo-123.json?baseUrl=http://issues.apache.org/jira')
|
||||
.get('/foo-123.json?baseUrl=https://issues.apache.org/jira')
|
||||
.intercept(nock =>
|
||||
nock('http://issues.apache.org/jira/rest/api/2/issue')
|
||||
nock('https://issues.apache.org/jira/rest/api/2/issue')
|
||||
.get(`/${encodeURIComponent('foo-123')}`)
|
||||
.reply(200, {
|
||||
fields: {
|
||||
|
||||
@@ -4,20 +4,20 @@ import { sprintId, sprintQueryString } from './jira-test-helpers.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('unknown sprint')
|
||||
.get('/abc.json?baseUrl=https://jira.spring.io')
|
||||
.get('/abc.json?baseUrl=https://issues.apache.org/jira')
|
||||
.expectBadge({ label: 'jira', message: 'sprint not found' })
|
||||
|
||||
t.create('known sprint')
|
||||
.get('/94.json?baseUrl=https://jira.spring.io')
|
||||
.get('/3.json?baseUrl=https://issues.apache.org/jira')
|
||||
.expectBadge({
|
||||
label: 'completion',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('100% completion')
|
||||
.get(`/${sprintId}.json?baseUrl=http://issues.apache.org/jira`)
|
||||
.get(`/${sprintId}.json?baseUrl=https://issues.apache.org/jira`)
|
||||
.intercept(nock =>
|
||||
nock('http://issues.apache.org/jira/rest/api/2')
|
||||
nock('https://issues.apache.org/jira/rest/api/2')
|
||||
.get('/search')
|
||||
.query(sprintQueryString)
|
||||
.reply(200, {
|
||||
@@ -47,9 +47,9 @@ t.create('100% completion')
|
||||
})
|
||||
|
||||
t.create('0% completion')
|
||||
.get(`/${sprintId}.json?baseUrl=http://issues.apache.org/jira`)
|
||||
.get(`/${sprintId}.json?baseUrl=https://issues.apache.org/jira`)
|
||||
.intercept(nock =>
|
||||
nock('http://issues.apache.org/jira/rest/api/2')
|
||||
nock('https://issues.apache.org/jira/rest/api/2')
|
||||
.get('/search')
|
||||
.query(sprintQueryString)
|
||||
.reply(200, {
|
||||
@@ -72,9 +72,9 @@ t.create('0% completion')
|
||||
})
|
||||
|
||||
t.create('no issues in sprint')
|
||||
.get(`/${sprintId}.json?baseUrl=http://issues.apache.org/jira`)
|
||||
.get(`/${sprintId}.json?baseUrl=https://issues.apache.org/jira`)
|
||||
.intercept(nock =>
|
||||
nock('http://issues.apache.org/jira/rest/api/2')
|
||||
nock('https://issues.apache.org/jira/rest/api/2')
|
||||
.get('/search')
|
||||
.query(sprintQueryString)
|
||||
.reply(200, {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import dayjs from 'dayjs'
|
||||
import { InvalidResponse, pathParams } from '../index.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { parseDate, renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import MavenCentralBase from './maven-central-base.js'
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const updateResponseSchema = Joi.object({
|
||||
metadata: Joi.object({
|
||||
@@ -38,13 +34,6 @@ export default class MavenCentralLastUpdate extends MavenCentralBase {
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
static render({ date }) {
|
||||
return {
|
||||
message: formatDate(date),
|
||||
color: ageColor(date),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ groupId, artifactId }) {
|
||||
const { metadata } = await this.fetch({
|
||||
groupId,
|
||||
@@ -52,15 +41,11 @@ export default class MavenCentralLastUpdate extends MavenCentralBase {
|
||||
schema: updateResponseSchema,
|
||||
})
|
||||
|
||||
const date = dayjs(
|
||||
const date = parseDate(
|
||||
String(metadata.versioning.lastUpdated),
|
||||
'YYYYMMDDHHmmss',
|
||||
)
|
||||
|
||||
if (!date.isValid) {
|
||||
throw new InvalidResponse({ prettyMessage: 'invalid date' })
|
||||
}
|
||||
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,6 @@ import {
|
||||
isMetric,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import {
|
||||
queryIndex,
|
||||
nuGetV3VersionJsonWithDash,
|
||||
nuGetV3VersionJsonFirstCharZero,
|
||||
nuGetV3VersionJsonFirstCharNotZero,
|
||||
} from '../nuget-fixtures.js'
|
||||
import { invalidJSON } from '../response-fixtures.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
@@ -75,66 +69,6 @@ t.create('version (tenant)')
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (yellow badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2-beta',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (orange badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (blue badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (not found)')
|
||||
.get('/myget/foo/v/not-a-real-package.json')
|
||||
.expectBadge({ label: 'myget', message: 'package not found' })
|
||||
@@ -148,66 +82,6 @@ t.create('version (pre) (valid)')
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (pre) (yellow badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2-beta',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (pre) (orange badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (pre) (blue badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (pre) (not found)')
|
||||
.get('/myget/foo/vpre/not-a-real-package.json')
|
||||
.expectBadge({ label: 'myget', message: 'package not found' })
|
||||
|
||||
@@ -9,14 +9,6 @@ const description = `<p>This badge indicates whether the package supports the <b
|
||||
export default class NodeCurrentVersion extends NodeVersionBase {
|
||||
static route = this.buildRoute('node/v', { withTag: true })
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'node',
|
||||
}
|
||||
|
||||
static type = 'current'
|
||||
|
||||
static colorResolver = versionColorForRangeCurrent
|
||||
|
||||
static openApi = {
|
||||
'/node/v/{packageName}': {
|
||||
get: {
|
||||
@@ -57,4 +49,12 @@ export default class NodeCurrentVersion extends NodeVersionBase {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'node',
|
||||
}
|
||||
|
||||
static type = 'current'
|
||||
|
||||
static colorResolver = versionColorForRangeCurrent
|
||||
}
|
||||
|
||||
@@ -9,14 +9,6 @@ const description = `<p>This badge indicates whether the package supports <b>all
|
||||
export default class NodeLtsVersion extends NodeVersionBase {
|
||||
static route = this.buildRoute('node/v-lts', { withTag: true })
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'node-lts',
|
||||
}
|
||||
|
||||
static type = 'lts'
|
||||
|
||||
static colorResolver = versionColorForRangeLts
|
||||
|
||||
static openApi = {
|
||||
'/node/v-lts/{packageName}': {
|
||||
get: {
|
||||
@@ -57,4 +49,12 @@ export default class NodeLtsVersion extends NodeVersionBase {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'node-lts',
|
||||
}
|
||||
|
||||
static type = 'lts'
|
||||
|
||||
static colorResolver = versionColorForRangeLts
|
||||
}
|
||||
|
||||
@@ -81,8 +81,11 @@ export default class NpmBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
async _requestJson(data) {
|
||||
return super._requestJson(
|
||||
this.authHelper.withBearerAuthHeader({
|
||||
let payload
|
||||
if (data?.options?.headers?.Accept) {
|
||||
payload = data
|
||||
} else {
|
||||
payload = {
|
||||
...data,
|
||||
options: {
|
||||
headers: {
|
||||
@@ -91,8 +94,9 @@ export default class NpmBase extends BaseJsonService {
|
||||
Accept: '*/*',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return super._requestJson(this.authHelper.withBearerAuthHeader(payload))
|
||||
}
|
||||
|
||||
async fetchPackageData({ registryUrl, scope, packageName, tag }) {
|
||||
@@ -143,4 +147,37 @@ export default class NpmBase extends BaseJsonService {
|
||||
|
||||
return this.constructor._validate(packageData, packageDataSchema)
|
||||
}
|
||||
|
||||
async fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema,
|
||||
abbreviated = false,
|
||||
}) {
|
||||
registryUrl = registryUrl || this.constructor.defaultRegistryUrl
|
||||
let url
|
||||
|
||||
if (scope === undefined) {
|
||||
url = `${registryUrl}/${packageName}`
|
||||
} else {
|
||||
const scoped = this.constructor.encodeScopedPackage({
|
||||
scope,
|
||||
packageName,
|
||||
})
|
||||
url = `${registryUrl}/${scoped}`
|
||||
}
|
||||
|
||||
// https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md
|
||||
const options = abbreviated
|
||||
? { headers: { Accept: 'application/vnd.npm.install-v1+json' } }
|
||||
: {}
|
||||
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema,
|
||||
options,
|
||||
httpErrors: { 404: 'package not found' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
119
services/npm/npm-last-update.service.js
Normal file
119
services/npm/npm-last-update.service.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import Joi from 'joi'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import NpmBase, {
|
||||
packageNameDescription,
|
||||
queryParamSchema,
|
||||
} from './npm-base.js'
|
||||
|
||||
const fullSchema = Joi.object({
|
||||
time: Joi.object()
|
||||
.pattern(Joi.string().required(), Joi.string().required())
|
||||
.required(),
|
||||
'dist-tags': Joi.object()
|
||||
.pattern(Joi.string().required(), Joi.string().required())
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const abbreviatedSchema = Joi.object({
|
||||
modified: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
export class NpmLastUpdateWithTag extends NpmBase {
|
||||
static category = 'activity'
|
||||
|
||||
static route = {
|
||||
base: 'npm/last-update',
|
||||
pattern: ':scope(@[^/]+)?/:packageName/:tag',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/npm/last-update/{packageName}/{tag}': {
|
||||
get: {
|
||||
summary: 'NPM Last Update (with dist tag)',
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'verdaccio',
|
||||
packageNameDescription,
|
||||
}),
|
||||
pathParam({
|
||||
name: 'tag',
|
||||
example: 'next-8',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
async handle(namedParams, queryParams) {
|
||||
const { scope, packageName, tag, registryUrl } =
|
||||
this.constructor.unpackParams(namedParams, queryParams)
|
||||
|
||||
const packageData = await this.fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema: fullSchema,
|
||||
})
|
||||
|
||||
const tagVersion = packageData['dist-tags'][tag]
|
||||
|
||||
if (!tagVersion) {
|
||||
throw new NotFound({ prettyMessage: 'tag not found' })
|
||||
}
|
||||
|
||||
return renderDateBadge(packageData.time[tagVersion])
|
||||
}
|
||||
}
|
||||
|
||||
export class NpmLastUpdate extends NpmBase {
|
||||
static category = 'activity'
|
||||
|
||||
static route = this.buildRoute('npm/last-update', { withTag: false })
|
||||
|
||||
static openApi = {
|
||||
'/npm/last-update/{packageName}': {
|
||||
get: {
|
||||
summary: 'NPM Last Update',
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'verdaccio',
|
||||
packageNameDescription,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
async handle(namedParams, queryParams) {
|
||||
const { scope, packageName, registryUrl } = this.constructor.unpackParams(
|
||||
namedParams,
|
||||
queryParams,
|
||||
)
|
||||
|
||||
const packageData = await this.fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema: abbreviatedSchema,
|
||||
abbreviated: true,
|
||||
})
|
||||
|
||||
return renderDateBadge(packageData.modified)
|
||||
}
|
||||
}
|
||||
81
services/npm/npm-last-update.tester.js
Normal file
81
services/npm/npm-last-update.tester.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { isFormattedDate } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('last updated date, no tag, valid package')
|
||||
.get('/verdaccio.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, invalid package')
|
||||
.get('/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, custom repository, valid package')
|
||||
.get('/verdaccio.json?registry_uri=https://registry.npmjs.com')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, valid package with scope')
|
||||
.get('/@npm/types.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, invalid package with scope')
|
||||
.get('/@not-a-scoped-package/not-a-valid-package.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, valid package')
|
||||
.get('/verdaccio/latest.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid package')
|
||||
.get('/not-a-package/doesnt-matter.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid tag')
|
||||
.get('/verdaccio/not-a-valid-tag.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'tag not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, custom repository, valid package')
|
||||
.get('/verdaccio/latest.json?registry_uri=https://registry.npmjs.com')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, valid package with scope')
|
||||
.get('/@npm/types/latest.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid package with scope')
|
||||
.get('/@not-a-scoped-package/not-a-valid-package/doesnt-matter.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const queryIndex = JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
'@id': 'https://api-v2v3search-0.nuget.org/query',
|
||||
'@type': 'SearchQueryService',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nuGetV3VersionJsonWithDash = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '1.2-beta' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const nuGetV3VersionJsonFirstCharZero = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '0.35' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const nuGetV3VersionJsonFirstCharNotZero = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '1.2.7' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nuGetV3VersionJsonBuildMetadataWithDash = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [
|
||||
{
|
||||
version: '1.16.0+388',
|
||||
},
|
||||
{
|
||||
version: '1.17.0+1b81349-429',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export {
|
||||
queryIndex,
|
||||
nuGetV3VersionJsonWithDash,
|
||||
nuGetV3VersionJsonFirstCharZero,
|
||||
nuGetV3VersionJsonFirstCharNotZero,
|
||||
nuGetV3VersionJsonBuildMetadataWithDash,
|
||||
}
|
||||
@@ -4,16 +4,35 @@ import {
|
||||
isVPlusDottedVersionNClauses,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import {
|
||||
queryIndex,
|
||||
nuGetV3VersionJsonFirstCharZero,
|
||||
nuGetV3VersionJsonFirstCharNotZero,
|
||||
nuGetV3VersionJsonBuildMetadataWithDash,
|
||||
} from '../nuget-fixtures.js'
|
||||
import { invalidJSON } from '../response-fixtures.js'
|
||||
|
||||
export const t = new ServiceTester({ id: 'nuget', title: 'NuGet' })
|
||||
|
||||
const queryIndex = JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
'@id': 'https://api-v2v3search-0.nuget.org/query',
|
||||
'@type': 'SearchQueryService',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nuGetV3VersionJsonBuildMetadataWithDash = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [
|
||||
{
|
||||
version: '1.16.0+388',
|
||||
},
|
||||
{
|
||||
version: '1.17.0+1b81349-429',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// downloads
|
||||
|
||||
t.create('total downloads (valid)')
|
||||
@@ -50,59 +69,6 @@ t.create('version (valid)')
|
||||
message: isVPlusDottedVersionNClauses,
|
||||
})
|
||||
|
||||
t.create('version (orange badge)')
|
||||
.get('/v/Microsoft.AspNetCore.Mvc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amicrosoft.aspnetcore.mvc&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (blue badge)')
|
||||
.get('/v/Microsoft.AspNetCore.Mvc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amicrosoft.aspnetcore.mvc&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/4219
|
||||
t.create('version (build metadata with -)')
|
||||
.get('/v/MongoFramework.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get('/query?q=packageid%3Amongoframework&prerelease=true&semVerLevel=2')
|
||||
.reply(200, nuGetV3VersionJsonBuildMetadataWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v1.17.0',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (not found)')
|
||||
.get('/v/not-a-real-package.json')
|
||||
.expectBadge({ label: 'nuget', message: 'package not found' })
|
||||
@@ -121,6 +87,23 @@ t.create('version (unexpected second response)')
|
||||
)
|
||||
.expectBadge({ label: 'nuget', message: 'unparseable json response' })
|
||||
|
||||
// https://github.com/badges/shields/issues/4219
|
||||
t.create('version (build metadata with -)')
|
||||
.get('/v/MongoFramework.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get('/query?q=packageid%3Amongoframework&prerelease=true&semVerLevel=2')
|
||||
.reply(200, nuGetV3VersionJsonBuildMetadataWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v1.17.0',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
// version (pre)
|
||||
|
||||
t.create('version (pre) (valid)')
|
||||
@@ -130,56 +113,6 @@ t.create('version (pre) (valid)')
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (pre) (orange badge)')
|
||||
.get('/vpre/Microsoft.AspNetCore.Mvc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amicrosoft.aspnetcore.mvc&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (pre) (blue badge)')
|
||||
.get('/vpre/Microsoft.AspNetCore.Mvc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amicrosoft.aspnetcore.mvc&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nuget',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (pre) (not found)')
|
||||
.get('/vpre/not-a-real-package.json')
|
||||
.expectBadge({ label: 'nuget', message: 'package not found' })
|
||||
|
||||
t.create('version (pre) (unexpected second response)')
|
||||
.get('/vpre/Microsoft.AspNetCore.Mvc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.nuget.org').get('/v3/index.json').reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amicrosoft.aspnetcore.mvc&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(invalidJSON),
|
||||
)
|
||||
.expectBadge({ label: 'nuget', message: 'unparseable json response' })
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { pathParams } from '../index.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { OpenVSXBase, description } from './open-vsx-base.js'
|
||||
|
||||
export default class OpenVSXReleaseDate extends OpenVSXBase {
|
||||
@@ -32,17 +31,8 @@ export default class OpenVSXReleaseDate extends OpenVSXBase {
|
||||
|
||||
static defaultBadgeData = { label: 'release date' }
|
||||
|
||||
static render({ releaseDate }) {
|
||||
return {
|
||||
message: formatDate(releaseDate),
|
||||
color: age(releaseDate),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ namespace, extension }) {
|
||||
const { timestamp } = await this.fetch({ namespace, extension })
|
||||
return this.constructor.render({
|
||||
releaseDate: timestamp,
|
||||
})
|
||||
return renderDateBadge(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import chai from 'chai'
|
||||
import { expect, use } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import PackagistDependencyVersion from './packagist-dependency-version.service.js'
|
||||
const { expect } = chai
|
||||
chai.use(chaiAsPromised)
|
||||
use(chaiAsPromised)
|
||||
|
||||
describe('PackagistDependencyVersion', function () {
|
||||
const fullPackagistJson = {
|
||||
|
||||
@@ -4,7 +4,7 @@ export const description = `
|
||||
[PingPong](https://pingpong.one/) is a status page and monitoring service.
|
||||
|
||||
To see more details about this badge and obtain your api key, visit
|
||||
[https://my.pingpong.one/integrations/badge-status/](https://my.pingpong.one/integrations/badge-status/)
|
||||
[https://my.pingpong.one/integrations/badge-uptime/](https://my.pingpong.one/integrations/badge-uptime/)
|
||||
`
|
||||
|
||||
export const baseUrl = 'https://api.pingpong.one/widget/shields'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user