1
0
mirror of https://github.com/badges/shields.git synced 2025-04-18 19:44:04 +03:00

Add renderDateBadge helper; affects [aur BitbucketLastCommit chrome date eclipse factorio galaxytoolshed GiteaLastCommit GistLastCommit GithubCreatedAt GithubHacktoberfest GithubIssueDetail GithubLastCommit GithubReleaseDate GitlabLastCommit maven npm openvsx snapcraft SourceforgeLastCommit steam vaadin visualstudio wordpress] (#10682)

* add and consistently use parseDate and renderDateBadge helpers

also move

- age
- formatDate
- formatRelativeDate

to date.js

* fix bug in wordpress last update badge

* validate in formatDate() and age()

it is going to be unlikely we'll invoke either of these
directly now, but lets calidate here too

* remove unusued imports

* reverse colours for galaxy toolshed
This commit is contained in:
chris48s 2024-11-17 13:15:28 +00:00 committed by GitHub
parent 4132ca2e7e
commit 5cdef88bcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 323 additions and 457 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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')

108
services/date.js Normal file
View 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
View 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')
})
})

View File

@ -1,4 +1,4 @@
import { formatRelativeDate } from '../text-formatters.js'
import { formatRelativeDate } from '../date.js'
import { BaseService, pathParams } from '../index.js'
const description = `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
import Joi from 'joi'
import dayjs from 'dayjs'
import { InvalidResponse, NotFound, pathParam, queryParam } from '../index.js'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { NotFound, pathParam, queryParam } from '../index.js'
import { renderDateBadge } from '../date.js'
import NpmBase, {
packageNameDescription,
queryParamSchema,
@ -21,26 +19,17 @@ const abbreviatedSchema = Joi.object({
modified: Joi.string().required(),
}).required()
class NpmLastUpdateBase extends NpmBase {
export class NpmLastUpdateWithTag extends NpmBase {
static category = 'activity'
static defaultBadgeData = { label: 'last updated' }
static render({ date }) {
return {
message: formatDate(date),
color: ageColor(date),
}
}
}
export class NpmLastUpdateWithTag extends NpmLastUpdateBase {
static route = {
base: 'npm/last-update',
pattern: ':scope(@[^/]+)?/:packageName/:tag',
queryParamSchema,
}
static defaultBadgeData = { label: 'last updated' }
static openApi = {
'/npm/last-update/{packageName}/{tag}': {
get: {
@ -81,19 +70,17 @@ export class NpmLastUpdateWithTag extends NpmLastUpdateBase {
throw new NotFound({ prettyMessage: 'tag not found' })
}
const date = dayjs(packageData.time[tagVersion])
if (!date.isValid) {
throw new InvalidResponse({ prettyMessage: 'invalid date' })
}
return this.constructor.render({ date })
return renderDateBadge(packageData.time[tagVersion])
}
}
export class NpmLastUpdate extends NpmLastUpdateBase {
export class NpmLastUpdate extends NpmBase {
static category = 'activity'
static route = this.buildRoute('npm/last-update', { withTag: false })
static defaultBadgeData = { label: 'last updated' }
static openApi = {
'/npm/last-update/{packageName}': {
get: {
@ -127,12 +114,6 @@ export class NpmLastUpdate extends NpmLastUpdateBase {
abbreviated: true,
})
const date = dayjs(packageData.modified)
if (!date.isValid) {
throw new InvalidResponse({ prettyMessage: 'invalid date' })
}
return this.constructor.render({ date })
return renderDateBadge(packageData.modified)
}
}

View File

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

View File

@ -1,8 +1,6 @@
import Joi from 'joi'
import dayjs from 'dayjs'
import { pathParams, queryParam, NotFound, InvalidResponse } from '../index.js'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { pathParams, queryParam, NotFound } from '../index.js'
import { renderDateBadge } from '../date.js'
import SnapcraftBase, { snapcraftPackageParam } from './snapcraft-base.js'
const queryParamSchema = Joi.object({
@ -57,13 +55,6 @@ export default class SnapcraftLastUpdate extends SnapcraftBase {
},
}
static render({ lastUpdatedDate }) {
return {
message: formatDate(lastUpdatedDate),
color: ageColor(lastUpdatedDate),
}
}
static transform(apiData, track, risk, arch) {
const channelMap = apiData['channel-map']
let filteredChannelMap = channelMap.filter(
@ -99,12 +90,6 @@ export default class SnapcraftLastUpdate extends SnapcraftBase {
arch,
)
const lastUpdatedDate = dayjs(channel['released-at'])
if (!lastUpdatedDate.isValid) {
throw new InvalidResponse({ prettyMessage: 'invalid date' })
}
return this.constructor.render({ lastUpdatedDate })
return renderDateBadge(channel['released-at'])
}
}

View File

@ -1,7 +1,6 @@
import Joi from 'joi'
import { BaseJsonService, pathParams } from '../index.js'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { renderDateBadge } from '../date.js'
const schema = Joi.object({
commits: Joi.array()
@ -35,13 +34,6 @@ export default class SourceforgeLastCommit extends BaseJsonService {
static defaultBadgeData = { label: 'last commit' }
static render({ commitDate }) {
return {
message: formatDate(new Date(commitDate)),
color: ageColor(new Date(commitDate)),
}
}
async fetch({ project }) {
return this._requestJson({
url: `https://sourceforge.net/rest/p/${project}/git/commits`,
@ -54,8 +46,6 @@ export default class SourceforgeLastCommit extends BaseJsonService {
async handle({ project }) {
const body = await this.fetch({ project })
return this.constructor.render({
commitDate: body.commits[0].committed_date,
})
return renderDateBadge(body.commits[0].committed_date)
}
}

View File

@ -1,8 +1,8 @@
import Joi from 'joi'
import prettyBytes from 'pretty-bytes'
import { renderDateBadge } from '../date.js'
import { renderDownloadsBadge } from '../downloads.js'
import { metric, formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { metric } from '../text-formatters.js'
import { NotFound, pathParams } from '../index.js'
import BaseSteamAPI from './steam-base.js'
@ -242,13 +242,9 @@ class SteamFileReleaseDate extends SteamFileService {
label: 'release date',
}
static render({ releaseDate }) {
return { message: formatDate(releaseDate), color: ageColor(releaseDate) }
}
async onRequest({ response }) {
const releaseDate = new Date(0).setUTCSeconds(response.time_created)
return this.constructor.render({ releaseDate })
return renderDateBadge(releaseDate)
}
}
@ -277,13 +273,9 @@ class SteamFileUpdateDate extends SteamFileService {
label: 'update date',
}
static render({ updateDate }) {
return { message: formatDate(updateDate), color: ageColor(updateDate) }
}
async onRequest({ response }) {
const updateDate = new Date(0).setUTCSeconds(response.time_updated)
return this.constructor.render({ updateDate })
return renderDateBadge(updateDate)
}
}

View File

@ -5,12 +5,6 @@
* @module
*/
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar.js'
import relativeTime from 'dayjs/plugin/relativeTime.js'
dayjs.extend(calendar)
dayjs.extend(relativeTime)
/**
* Creates a string of stars and empty stars based on the rating.
* The number of stars is determined by the integer part of the rating.
@ -165,39 +159,6 @@ function maybePluralize(singular, countable, plural) {
}
}
/**
* Returns a formatted date string without the year based on the value of input date param d.
*
* @param {Date | string | number | object } d - Input date in dayjs compatible format, date object, datestring, Unix timestamp etc.
* @returns {string} Formatted date string
*/
function formatDate(d) {
const date = dayjs(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()
}
/**
* 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 {
starRating,
currencyFromCode,
@ -206,6 +167,4 @@ export {
omitv,
addv,
maybePluralize,
formatDate,
formatRelativeDate,
}

View File

@ -1,5 +1,4 @@
import { test, given } from 'sazerac'
import sinon from 'sinon'
import {
starRating,
currencyFromCode,
@ -8,8 +7,6 @@ import {
omitv,
addv,
maybePluralize,
formatDate,
formatRelativeDate,
} from './text-formatters.js'
describe('Text formatters', function () {
@ -113,54 +110,4 @@ describe('Text formatters', function () {
given('box', [123, 456], 'boxes').expect('boxes')
given('box', undefined, 'boxes').expect('boxes')
})
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')
})
})
})

View File

@ -1,6 +1,5 @@
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 { BaseVaadinDirectoryService } from './vaadin-directory-base.js'
export default class VaadinDirectoryReleaseDate extends BaseVaadinDirectoryService {
@ -27,14 +26,8 @@ export default class VaadinDirectoryReleaseDate extends BaseVaadinDirectoryServi
label: 'latest release date',
}
static render({ date }) {
return { message: formatDate(date), color: ageColor(date) }
}
async handle({ alias, packageName }) {
const data = await this.fetch({ packageName })
return this.constructor.render({
date: data.latestAvailableRelease.publicationDate,
})
return renderDateBadge(data.latestAvailableRelease.publicationDate)
}
}

View File

@ -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 VisualStudioMarketplaceBase from './visual-studio-marketplace-base.js'
export default class VisualStudioMarketplaceLastUpdated extends VisualStudioMarketplaceBase {
@ -28,13 +27,6 @@ export default class VisualStudioMarketplaceLastUpdated extends VisualStudioMark
label: 'last updated',
}
static render({ lastUpdated }) {
return {
message: formatDate(lastUpdated),
color: age(lastUpdated),
}
}
transform({ json }) {
const { extension } = this.transformExtension({ json })
const lastUpdated = extension.lastUpdated
@ -44,6 +36,6 @@ export default class VisualStudioMarketplaceLastUpdated extends VisualStudioMark
async handle({ extensionId }) {
const json = await this.fetch({ extensionId })
const { lastUpdated } = this.transform({ json })
return this.constructor.render({ lastUpdated })
return renderDateBadge(lastUpdated)
}
}

View File

@ -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 VisualStudioMarketplaceBase from './visual-studio-marketplace-base.js'
export default class VisualStudioMarketplaceReleaseDate extends VisualStudioMarketplaceBase {
@ -28,13 +27,6 @@ export default class VisualStudioMarketplaceReleaseDate extends VisualStudioMark
label: 'release date',
}
static render({ releaseDate }) {
return {
message: formatDate(releaseDate),
color: age(releaseDate),
}
}
transform({ json }) {
const { extension } = this.transformExtension({ json })
const releaseDate = extension.releaseDate
@ -44,6 +36,6 @@ export default class VisualStudioMarketplaceReleaseDate extends VisualStudioMark
async handle({ extensionId }) {
const json = await this.fetch({ extensionId })
const { releaseDate } = this.transform({ json })
return this.constructor.render({ releaseDate })
return renderDateBadge(releaseDate)
}
}

View File

@ -1,16 +1,12 @@
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
import { InvalidResponse, pathParams } from '../index.js'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { pathParams } from '../index.js'
import { parseDate, renderDateBadge } from '../date.js'
import { description, 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 h:mma [GMT]',
},
theme: {
capt: 'Theme',
@ -50,35 +46,15 @@ function LastUpdateForType(extensionType) {
static defaultBadgeData = { label: 'last updated' }
static render({ lastUpdated }) {
return {
label: 'last updated',
message: formatDate(lastUpdated),
color: ageColor(lastUpdated),
}
}
transform(lastUpdate) {
const date = dayjs(lastUpdate, lastUpdateFormat)
if (date.isValid()) {
return date.format('YYYY-MM-DD')
} else {
throw new InvalidResponse({ prettyMessage: 'invalid date' })
}
}
async handle({ slug }) {
const { last_updated: lastUpdated } = await this.fetch({
extensionType,
slug,
})
const newDate = this.transform(lastUpdated)
const date = parseDate(lastUpdated, lastUpdateFormat)
return this.constructor.render({
lastUpdated: newDate,
})
return renderDateBadge(date)
}
}
}