Compare commits

..

1 Commits

Author SHA1 Message Date
Caleb Cartwright
7da38f8354 docs: start collating info on integration patterns 2022-07-14 21:11:16 -05:00
31 changed files with 844 additions and 1103 deletions

View File

@@ -149,8 +149,6 @@ jobs:
main@node-17:
docker:
- image: cimg/node:17.9
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *main_steps
@@ -165,8 +163,6 @@ jobs:
docker:
- image: cimg/node:17.9
- image: redis
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *integration_steps
@@ -243,8 +239,6 @@ jobs:
services@node-17:
docker:
- image: cimg/node:17.9
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *services_steps

View File

@@ -4,23 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
---
## server-2022-08-01
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
- Fix wording in code-walkthrough.md [#8240](https://github.com/badges/shields/issues/8240)
- Fix typo in service-tests.md [#8239](https://github.com/badges/shields/issues/8239)
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
- Fix heroku review apps (sort of) [#8213](https://github.com/badges/shields/issues/8213)
- Fix missing `dayjs` -> `moment` [#8204](https://github.com/badges/shields/issues/8204)
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
- add docs on nouns in route [#8141](https://github.com/badges/shields/issues/8141)
- Dependency updates
## server-2022-07-03
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)

View File

@@ -35,16 +35,6 @@
"WEBLATE_API_KEY": {
"description": "Configure the API key to be used for the Weblate service.",
"required": false
},
"METRICS_INFLUX_ENABLED": {
"description": "Disable influx metrics",
"value": "false",
"required": false
},
"REQUIRE_CLOUDFLARE": {
"description": "Allow direct traffic",
"value": "false",
"required": false
}
},
"formation": {

View File

@@ -58,7 +58,7 @@ The tests are also divided into several parts:
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
Our goal is to reach 100% coverage of the code in the
Our goal is for the core code is to reach 100% coverage of the code in the
frontend, core, and service helper functions when the unit and functional
tests are run.
@@ -95,7 +95,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
callback with the four parameters `( queryParams, match, end, ask )` which
is created in a legacy helper function in
[`legacy-request-handler.js`][legacy-request-handler]. This callback
delegates to a callback in `BaseService.register` with three different
delegates to a callback in `BaseService.register` with four different
parameters `( queryParams, match, sendBadge )`, which
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
service and runs `BaseService#handle`.

View File

@@ -0,0 +1,75 @@
# Integration with upstream services
## Overview
In a nutshell, the Shields Badge Server handles the responsibilities of accepting requests for badges, and then serving those badges back to users.
A grossly oversimplified visualization would probably look something like this:
```mermaid
sequenceDiagram
actor User
participant B as Badge Server
participant P as Data Provider
User->>+B: I'd like a badge for my project please!
B->>+P: Get data for project
P-->>-B: Data
B->>B: Make badge with data
B-->>-User: Here's your badge!
```
Shields is not a system of record (we're not the package registry, pipeline tool, etc.) so when Shields receives a request for a badge, the badge server will first have to reach out to the system of record in order to get the data points it needs to create your badge.
For example, if you ask Shields for a build status badge for your CircleCI pipeline, then Shields has to reach out to CircleCI to figure out what the status of your pipeline is (CircleCI would be the "Data Provider" actor in the above diagram). Similarly if you want a badge that shows the count of downloads of your npm package, Shields has to reach out to the npm.js registry.
That covers the gist, but the actual story is a bit more involved and complicated than that of course. There's a number of other components along the way, ranging from the browser on your local machine to extra services and actors we have deployed as part of the Shields.io runtime ecosystem which help ensure we can provide a stable and reliable deployment of the badge server-as-a-service. Additionally, badges rendered in GitHub have some additional factors at play that impose some additional constraints, detailed in the next section.
### GitHub Badge Rendering
A common usage pattern for badges is to embed them in your project's README files so relevant information is conveyed to the project's users. This means badges are often utilized and rendered in source control management platforms, like GitHub.
[GitHub utilizes a proxy service, called camo][camo], for handling the images that you see when you browse project pages on github.com, and this is utilized for badges too (both svg and png formats). GitHub does this for a number of reasons, including to anonymize requests and protect your privacy. However, this also requires the upstream images (including badges) to be returned quickly in order for those images to show up on your screen, with a rough ceiling of 3-4 seconds. If the upstream image provider is too slow to respond, then camo will timeout and the image won't be displayed.
This imposes constraints on Shields, as we need to ensure that the badge server completes the entire request/response workflow and returns the final badge within a few seconds.
[camo]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls
## Interaction patterns
Because of the time limits around the full badge flow discussed above, the badge server is somewhat constrained in terms of patterns it can employ to fetch data from the upstream provider. When we incorporate new badges we typically need to ensure when we receive a badge request and need to get data to serve that badge request, that we can get that data by making a single call to the upstream provider to get the data points needed for the badge.
The nature of the call Shields makes to upstream data providers is obviously dependent upon the nature of the upstream endpoints. The overwhelming majority of the time this occurs via a GET request, though there are a few occasions where the upstream endpoint requires a POST in order for us to retrieve data, and a few others where we issue a HEAD request because the data points we need for the badge actually reside in the response headers.
There are a couple other exceptions, but as a general pattern we strive to integrate with services via a stateless, single call manner.
### Authentication
Shields typically integrates with upstream data providers anonymously, largely because the data targets we need are anonymously available (open source packages, repositories, pipelines, etc.)
The badge server can be configured to make authenticated requests for certain supported services. This exists so that users who are [self-hosting] their own instance of the badge server can get badges for their private content, and also as part of agreements we've made with certain upstream data providers for the main Shields.io deployment.
[self-hosting]: ./self-hosting.md
### Rate Limits
Many upstream data providers employ common techniques to protect the availability and integrity of their service, and one common technique is [rate limiting].
Typically, when clients/consumers of rate limited endpoints will employ client-side techniques to avoid running afoul of those limits and potentially getting blocked or having their requests throttled. Unfortunately, those techniques aren't really viable for the Shields.io environment due to the workflow and constraints discussed above.
As such, we instead try to ensure that Shields.io never makes more calls to an upstream provider than their rate limits allow.
In cases where Shields.io may run close to or exceed those limits, we typically consider:
- increasing the cache periods we set (to reduce the number of badge requests we receive)
- collaborating with the vendor or maintainers of the upstream data provider to explore options for an increased rate limit for Shields.io
- decline to provide badges for that upstream data provider
[rate limiting]: https://en.wikipedia.org/wiki/Rate_limiting
### Denial of Service
coming soon...
### Considerations for new upstream integrations
coming soon...

View File

@@ -67,7 +67,7 @@ t.create('Build status')
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate ![](https://img.shields.io/wercker/build/wercker/go-wercker-api.svg) we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
Joi is a validation library that is built into IcedFrisby which you can use to
Joi is a validation library that is build into IcedFrisby which you can use to
match based on a set of allowed strings, regexes, or specific values. You can
refer to their [API reference][joi api].
4. We expect `label` to be a string literal `"build"`.

1393
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"@fontsource/lato": "^4.5.8",
"@fontsource/lekton": "^4.5.9",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^7.8.0",
"@sentry/node": "^7.5.1",
"@shields_io/camp": "^18.1.1",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
@@ -34,17 +34,16 @@
"cloudflare-middleware": "^1.0.4",
"config": "^3.3.7",
"cross-env": "^7.0.3",
"dayjs": "^1.11.4",
"decamelize": "^3.2.0",
"emojic": "^1.1.17",
"escape-string-regexp": "^4.0.0",
"fast-xml-parser": "^4.0.9",
"fast-xml-parser": "^4.0.8",
"glob": "^8.0.3",
"global-agent": "^3.0.0",
"got": "^12.3.0",
"got": "^12.1.0",
"graphql": "^15.6.1",
"graphql-tag": "^2.12.6",
"ioredis": "5.2.2",
"ioredis": "5.1.0",
"joi": "17.6.0",
"joi-extension-semver": "5.0.0",
"js-yaml": "^4.1.0",
@@ -52,6 +51,7 @@
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.29.4",
"node-env-flag": "^0.1.0",
"parse-link-header": "^2.0.0",
"path-to-regexp": "^6.2.1",
@@ -61,7 +61,7 @@
"qs": "^6.11.0",
"query-string": "^7.1.1",
"semver": "~7.3.7",
"simple-icons": "7.5.0",
"simple-icons": "7.4.0",
"webextension-store-meta": "^1.0.5",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
@@ -141,9 +141,9 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/core": "^7.18.6",
"@babel/polyfill": "^7.12.1",
"@babel/register": "7.18.9",
"@babel/register": "7.18.6",
"@istanbuljs/schema": "^0.1.3",
"@mapbox/react-click-to-select": "^2.2.1",
"@types/chai": "^4.3.1",
@@ -155,11 +155,11 @@
"@types/react-modal": "^3.13.1",
"@types/react-select": "^4.0.17",
"@types/styled-components": "5.1.25",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.30.7",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-preset-gatsby": "^2.19.0",
"c8": "^7.12.0",
"babel-preset-gatsby": "^2.14.0",
"c8": "^7.11.3",
"caller": "^1.1.0",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
@@ -167,8 +167,8 @@
"chai-string": "^1.4.0",
"child-process-promise": "^2.2.1",
"clipboard-copy": "^4.0.1",
"concurrently": "^7.3.0",
"cypress": "^10.3.1",
"concurrently": "^7.2.2",
"cypress": "^10.3.0",
"danger": "^11.1.1",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
@@ -181,7 +181,7 @@
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.3",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-mocha": "^10.0.5",
"eslint-plugin-no-extension-in-require": "^0.2.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.2.0",
@@ -191,18 +191,18 @@
"fetch-ponyfill": "^7.1.0",
"form-data": "^4.0.0",
"gatsby": "4.6.2",
"gatsby-plugin-catch-links": "^4.19.0",
"gatsby-plugin-catch-links": "^4.11.0",
"gatsby-plugin-page-creator": "^4.7.0",
"gatsby-plugin-react-helmet": "^5.10.0",
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
"gatsby-plugin-styled-components": "^5.19.0",
"gatsby-plugin-styled-components": "^5.11.0",
"gatsby-plugin-typescript": "^4.11.1",
"humanize-string": "^2.1.0",
"icedfrisby": "4.0.0",
"icedfrisby-nock": "^2.1.0",
"is-svg": "^4.3.2",
"js-yaml-loader": "^1.2.2",
"jsdoc": "^3.6.11",
"jsdoc": "^3.6.10",
"lint-staged": "^13.0.3",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
@@ -211,7 +211,7 @@
"mocha-env-reporter": "^4.0.0",
"mocha-junit-reporter": "^2.0.2",
"mocha-yaml-loader": "^1.0.3",
"nock": "13.2.9",
"nock": "13.2.8",
"node-mocks-http": "^1.11.0",
"nodemon": "^2.0.19",
"npm-run-all": "^4.1.5",
@@ -241,7 +241,7 @@
"url": "^0.11.0"
},
"engines": {
"node": "^16.13.0",
"node": ">=16.13.0",
"npm": ">=8.0.0"
},
"type": "module",

View File

@@ -5,7 +5,7 @@
* @module
*/
import dayjs from 'dayjs'
import moment from 'moment'
import pep440 from '@renovate/pep440'
/**
@@ -182,7 +182,7 @@ function colorScale(steps, colors, reversed) {
*/
function age(date) {
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, true)
const daysElapsed = dayjs().diff(dayjs(date), 'days')
const daysElapsed = moment().diff(moment(date), 'days')
return colorByAge(daysElapsed)
}

View File

@@ -1,41 +0,0 @@
import { formatDate } from '../text-formatters.js'
import BaseGalaxyToolshedService from './galaxytoolshed-base.js'
export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService {
static category = 'activity'
static route = {
base: 'galaxytoolshed/created-date',
pattern: ':repository/:owner',
}
static examples = [
{
title: 'Galaxy Toolshed (created date)',
namedParams: {
repository: 'sra_tools',
owner: 'iuc',
},
staticPreview: this.render({
date: this.render({ date: '2022-01-01' }),
}),
},
]
static defaultBadgeData = {
label: 'created date',
color: 'blue',
}
static render({ date }) {
return { message: formatDate(date) }
}
async handle({ repository, owner }) {
const response = await this.fetchLastOrderedInstallableRevisionsSchema({
repository,
owner,
})
const { create_time: date } = response[0]
return this.constructor.render({ date })
}
}

View File

@@ -1,23 +0,0 @@
import { isFormattedDate } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('Created Date')
.get('/sra_tools/iuc.json')
.expectBadge({ label: 'created date', message: isFormattedDate })
t.create('Created Date - repository not found')
.get('/sra_tool/iuc.json')
.expectBadge({ label: 'created date', message: 'not found' })
t.create('Created Date - owner not found')
.get('/sra_tools/iu.json')
.expectBadge({ label: 'created date', message: 'not found' })
t.create('Created Date - changesetRevision not found')
.get('/bioqc/badilla.json')
.expectBadge({
label: 'created date',
message: 'changesetRevision not found',
})

View File

@@ -9,7 +9,6 @@ const orderedInstallableRevisionsSchema = Joi.array()
const repositoryRevisionInstallInfoSchema = Joi.array()
.ordered(
Joi.object({
create_time: Joi.date().required(),
times_downloaded: nonNegativeInteger,
}).required()
)

View File

@@ -1,6 +1,6 @@
import gql from 'graphql-tag'
import Joi from 'joi'
import dayjs from 'dayjs'
import moment from 'moment'
import { metric, maybePluralize } from '../text-formatters.js'
import { nonNegativeInteger } from '../validators.js'
import { GithubAuthV4Service } from './github-auth-service.js'
@@ -121,7 +121,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 = moment(`${year}-11-01 12:00:00 Z`).diff(moment(), 'days') + 1
}
if (daysLeft < 0) {
return {
@@ -205,7 +205,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 = moment(`${year}-10-01 00:00:00 Z`).diff(
moment(),
'days'
)
const isBefore = daysToStart > 0
return { daysToStart, isBefore }
}

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
import Joi from 'joi'
import { age } from '../color-formatters.js'
import { formatDate } from '../text-formatters.js'
@@ -51,7 +51,7 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
static defaultBadgeData = { label: 'release date' }
static render({ date }) {
const releaseDate = dayjs(date)
const releaseDate = moment(date)
return {
message: formatDate(releaseDate),
color: age(releaseDate),

View File

@@ -1,77 +0,0 @@
import Joi from 'joi'
import { optionalUrl, nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import GitLabBase from './gitlab-base.js'
const schema = Joi.object({
forks_count: nonNegativeInteger,
}).required()
const queryParamSchema = Joi.object({
gitlab_url: optionalUrl,
}).required()
const documentation = `
<p>
You may use your GitLab Project Id (e.g. 278964) or your Project Path (e.g. gitlab-org/gitlab ).
Note that only internet-accessible GitLab instances are supported, for example https://jihulab.com, https://gitlab.gnome.org, or https://gitlab.com/.
</p>
`
export default class GitlabForks extends GitLabBase {
static category = 'social'
static route = {
base: 'gitlab/forks',
pattern: ':project+',
queryParamSchema,
}
static examples = [
{
title: 'GitLab forks',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'Fork',
message: '6.4k',
style: 'social',
},
documentation,
},
]
static defaultBadgeData = { label: 'forks', namedLogo: 'gitlab' }
static render({ baseUrl, project, forkCount }) {
return {
message: metric(forkCount),
color: 'blue',
link: [
`${baseUrl}/${project}/-/forks/new`,
`${baseUrl}/${project}/-/forks`,
],
}
}
async fetch({ project, baseUrl }) {
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
return super.fetch({
schema,
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(project)}`,
errorMessages: {
404: 'project not found',
},
})
}
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
const { forks_count: forkCount } = await this.fetch({
project,
baseUrl,
})
return this.constructor.render({ baseUrl, project, forkCount })
}
}

View File

@@ -1,35 +0,0 @@
import { isMetric } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('Forks')
.get('/gitlab-org/gitlab.json')
.expectBadge({
label: 'forks',
message: isMetric,
color: 'blue',
link: [
'https://gitlab.com/gitlab-org/gitlab/-/forks/new',
'https://gitlab.com/gitlab-org/gitlab/-/forks',
],
})
t.create('Forks (self-managed)')
.get('/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com')
.expectBadge({
label: 'forks',
message: isMetric,
color: 'blue',
link: [
'https://jihulab.com/gitlab-cn/gitlab/-/forks/new',
'https://jihulab.com/gitlab-cn/gitlab/-/forks',
],
})
t.create('Forks (project not found)')
.get('/user1/gitlab-does-not-have-this-repo.json')
.expectBadge({
label: 'forks',
message: 'project not found',
})

View File

@@ -1,40 +0,0 @@
import { floorCount as floorCountColor } from '../color-formatters.js'
import { metric } from '../text-formatters.js'
import BaseGreasyForkService from './greasyfork-base.js'
export default class GreasyForkRatingCount extends BaseGreasyForkService {
static category = 'rating'
static route = { base: 'greasyfork', pattern: 'rating-count/:scriptId' }
static examples = [
{
title: 'Greasy Fork',
namedParams: { scriptId: '407466' },
staticPreview: this.render({ good: 17, ok: 2, bad: 3 }),
},
]
static defaultBadgeData = { label: 'rating' }
static render({ good, ok, bad }) {
let color = 'lightgrey'
const total = good + bad + ok
if (total > 0) {
const score = (good * 3 + ok * 2 + bad * 1) / total - 1
color = floorCountColor(score, 1, 1.5, 2)
}
return {
message: `${metric(good)} good, ${metric(ok)} ok, ${metric(bad)} bad`,
color,
}
}
async handle({ scriptId }) {
const data = await this.fetch({ scriptId })
return this.constructor.render({
good: data.good_ratings,
ok: data.ok_ratings,
bad: data.bad_ratings,
})
}
}

View File

@@ -1,31 +0,0 @@
import { test, given } from 'sazerac'
import GreasyForkRatingCount from './greasyfork-rating.service.js'
describe('GreasyForkRatingCount', function () {
test(GreasyForkRatingCount.render, () => {
given({ good: 0, ok: 0, bad: 30 }).expect({
message: '0 good, 0 ok, 30 bad',
color: 'red',
})
given({ good: 10, ok: 20, bad: 30 }).expect({
message: '10 good, 20 ok, 30 bad',
color: 'yellow',
})
given({ good: 10, ok: 20, bad: 10 }).expect({
message: '10 good, 20 ok, 10 bad',
color: 'yellowgreen',
})
given({ good: 20, ok: 10, bad: 0 }).expect({
message: '20 good, 10 ok, 0 bad',
color: 'green',
})
given({ good: 30, ok: 0, bad: 0 }).expect({
message: '30 good, 0 ok, 0 bad',
color: 'brightgreen',
})
given({ good: 0, ok: 0, bad: 0 }).expect({
message: '0 good, 0 ok, 0 bad',
color: 'lightgrey',
})
})
})

View File

@@ -1,14 +0,0 @@
import Joi from 'joi'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('Rating Count')
.get('/rating-count/407466.json')
.expectBadge({
label: 'rating',
message: Joi.string().regex(/^\d+ good, \d+ ok, \d+ bad$/),
})
t.create('Rating Count (not found)')
.get('/rating-count/000000.json')
.expectBadge({ label: 'rating', message: 'not found' })

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
import semver from 'semver'
import { getCachedResource } from '../../core/base-service/resource-cache.js'
@@ -23,7 +23,7 @@ async function getVersion(version) {
}
function ltsVersionsScraper(versions) {
const currentDate = dayjs().format(dateFormat)
const currentDate = moment().format(dateFormat)
return Object.keys(versions).filter(function (version) {
const data = versions[version]
return data.lts && data.lts < currentDate && data.end > currentDate

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import dayjs from 'dayjs'
import moment from 'moment'
const dateFormat = 'YYYY-MM-DD'
@@ -67,7 +67,7 @@ const mockVersionsSha = () => nock => {
}
const mockReleaseSchedule = () => nock => {
const currentDate = dayjs()
const currentDate = moment()
const schedule = {
'v0.10': {
start: '2013-03-11',

View File

@@ -9,11 +9,16 @@ const schema = Joi.object({
license: Joi.string().allow('').allow(null),
classifiers: Joi.array().items(Joi.string()).required(),
}).required(),
urls: Joi.array()
.items(
Joi.object({
packagetype: Joi.string().required(),
})
releases: Joi.object()
.pattern(
Joi.string(),
Joi.array()
.items(
Joi.object({
packagetype: Joi.string().required(),
})
)
.required()
)
.required(),
}).required()

View File

@@ -88,12 +88,16 @@ function getLicenses(packageData) {
}
function getPackageFormats(packageData) {
const { urls } = packageData
const {
info: { version },
releases,
} = packageData
const releasesForVersion = releases[version]
return {
hasWheel: urls.some(({ packagetype }) =>
hasWheel: releasesForVersion.some(({ packagetype }) =>
['wheel', 'bdist_wheel'].includes(packagetype)
),
hasEgg: urls.some(({ packagetype }) =>
hasEgg: releasesForVersion.some(({ packagetype }) =>
['egg', 'bdist_egg'].includes(packagetype)
),
}

View File

@@ -164,17 +164,34 @@ describe('PyPI helpers', function () {
test(getPackageFormats, () => {
given({
urls: [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
info: { version: '2.19.1' },
releases: {
'1.0.4': [{ packagetype: 'sdist' }],
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
},
}).expect({ hasWheel: true, hasEgg: false })
given({
urls: [{ packagetype: 'sdist' }],
info: { version: '1.0.4' },
releases: {
'1.0.4': [{ packagetype: 'sdist' }],
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
},
}).expect({ hasWheel: false, hasEgg: false })
given({
urls: [
{ packagetype: 'bdist_egg' },
{ packagetype: 'bdist_egg' },
{ packagetype: 'sdist' },
],
info: { version: '0.8.2' },
releases: {
0.8: [{ packagetype: 'sdist' }],
'0.8.1': [
{ packagetype: 'bdist_egg' },
{ packagetype: 'bdist_egg' },
{ packagetype: 'sdist' },
],
'0.8.2': [
{ packagetype: 'bdist_egg' },
{ packagetype: 'bdist_egg' },
{ packagetype: 'sdist' },
],
},
}).expect({ hasWheel: false, hasEgg: true })
})
})

View File

@@ -24,7 +24,7 @@ t.create('license (from trove classifier)')
license: '',
classifiers: ['License :: OSI Approved :: MIT License'],
},
urls: [],
releases: {},
})
)
.expectBadge({
@@ -46,7 +46,7 @@ t.create('license (as acronym from trove classifier)')
'License :: OSI Approved :: GNU General Public License (GPL)',
],
},
urls: [],
releases: {},
})
)
.expectBadge({

View File

@@ -43,7 +43,7 @@ t.create('no trove classifiers')
license: 'foo',
classifiers: [],
},
urls: [],
releases: {},
})
)
.expectBadge({

View File

@@ -1,5 +1,5 @@
import Joi from 'joi'
import dayjs from 'dayjs'
import moment from 'moment'
import { renderDownloadsBadge } from '../downloads.js'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
@@ -15,15 +15,15 @@ const intervalMap = {
},
dw: {
// 6 days, since date range is inclusive,
startDate: endDate => dayjs(endDate).subtract(6, 'days'),
startDate: endDate => moment(endDate).subtract(6, 'days'),
interval: 'week',
},
dm: {
startDate: endDate => dayjs(endDate).subtract(30, 'days'),
startDate: endDate => moment(endDate).subtract(30, 'days'),
interval: 'month',
},
dt: {
startDate: () => dayjs(0),
startDate: () => moment(0),
},
}
@@ -78,7 +78,7 @@ export default class Sourceforge extends BaseJsonService {
folder ? `${folder}/` : ''
}stats/json`
// get yesterday since today is incomplete
const endDate = dayjs().subtract(24, 'hours')
const endDate = moment().subtract(24, 'hours')
const startDate = intervalMap[interval].startDate(endDate)
const options = {
searchParams: {

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
import Joi from 'joi'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
@@ -19,10 +19,10 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
static examples = [
{
title: 'Stack Exchange monthly questions',
namedParams: { stackexchangesite: 'stackoverflow', query: 'dayjs' },
namedParams: { stackexchangesite: 'stackoverflow', query: 'momentjs' },
staticPreview: this.render({
stackexchangesite: 'stackoverflow',
query: 'dayjs',
query: 'momentjs',
numValue: 2000,
}),
keywords: ['stackexchange', 'stackoverflow'],
@@ -41,12 +41,12 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
}
async handle({ stackexchangesite, query }) {
const today = dayjs().toDate()
const prevMonthStart = dayjs(today)
const today = moment().toDate()
const prevMonthStart = moment(today)
.subtract(1, 'months')
.startOf('month')
.unix()
const prevMonthEnd = dayjs(today)
const prevMonthEnd = moment(today)
.subtract(1, 'months')
.endOf('month')
.unix()

View File

@@ -2,10 +2,10 @@ import { isMetricOverTimePeriod } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('Monthly Questions for StackOverflow dayjs')
.get('/stackoverflow/qm/dayjs.json')
t.create('Monthly Questions for StackOverflow Momentjs')
.get('/stackoverflow/qm/momentjs.json')
.expectBadge({
label: 'stackoverflow dayjs questions',
label: 'stackoverflow momentjs questions',
message: isMetricOverTimePeriod,
})

View File

@@ -2,11 +2,8 @@
* Commonly-used functions for formatting text in badge labels. Includes
* ordinal numbers, currency codes, star ratings, versions, etc.
*/
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar.js'
import relativeTime from 'dayjs/plugin/relativeTime.js'
dayjs.extend(calendar)
dayjs.extend(relativeTime)
import moment from 'moment'
moment().format()
function starRating(rating, max = 5) {
const flooredRating = Math.floor(rating)
@@ -112,7 +109,7 @@ function maybePluralize(singular, countable, plural) {
}
function formatDate(d) {
const date = dayjs(d)
const date = moment(d)
const dateString = date.calendar(null, {
lastDay: '[yesterday]',
sameDay: '[today]',
@@ -120,12 +117,12 @@ function formatDate(d) {
sameElse: 'MMMM YYYY',
})
// Trim current year from date string
return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
return dateString.replace(` ${moment().year()}`, '').toLowerCase()
}
function formatRelativeDate(timestamp) {
return dayjs()
.to(dayjs.unix(parseInt(timestamp, 10)))
return moment()
.to(moment.unix(parseInt(timestamp, 10)))
.toLowerCase()
}

View File

@@ -1,16 +1,14 @@
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
import moment from 'moment'
import { InvalidResponse } from '../index.js'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { documentation, BaseWordpress } from './wordpress-base.js'
dayjs.extend(customParseFormat)
const extensionData = {
plugin: {
capt: 'Plugin',
exampleSlug: 'bbpress',
lastUpdateFormat: 'YYYY-MM-DD hh:mma [GMT]',
lastUpdateFormat: 'YYYY-MM-DD hh:mma GMT',
},
theme: {
capt: 'Theme',
@@ -52,7 +50,7 @@ function LastUpdateForType(extensionType) {
}
transform(lastUpdate) {
const date = dayjs(lastUpdate, lastUpdateFormat)
const date = moment(lastUpdate, lastUpdateFormat)
if (date.isValid()) {
return date.format('YYYY-MM-DD')