kopia lustrzana https://github.com/badgen/badgen.net
feat: add create-badgen-handler.ts
rodzic
f236e8e432
commit
0b55955576
|
@ -1,18 +1,13 @@
|
||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
import got from '../libs/got'
|
import got from '../libs/got'
|
||||||
import { millify, version, versionColor } from '../libs/utils'
|
import { millify, version, versionColor } from '../libs/utils'
|
||||||
import {
|
import { createBadgenHandler, BadgenServeConfig, PathArgs } from '../libs/create-badgen-handler'
|
||||||
badgenServe,
|
|
||||||
BadgenServeMeta as Meta,
|
|
||||||
BadgenServeHandlers as Handlers,
|
|
||||||
BadgenServeHandlerArgs as Args
|
|
||||||
} from '../libs/badgen-serve'
|
|
||||||
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md
|
// https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md
|
||||||
// https://github.com/npm/registry/blob/master/docs/download-counts.md
|
// https://github.com/npm/registry/blob/master/docs/download-counts.md
|
||||||
// https://unpkg.com/
|
// https://unpkg.com/
|
||||||
|
|
||||||
export const meta: Meta = {
|
export const config: BadgenServeConfig = {
|
||||||
title: 'npm',
|
title: 'npm',
|
||||||
examples: {
|
examples: {
|
||||||
'/npm/v/express': 'version',
|
'/npm/v/express': 'version',
|
||||||
|
@ -28,17 +23,16 @@ export const meta: Meta = {
|
||||||
'/npm/license/lodash': 'license',
|
'/npm/license/lodash': 'license',
|
||||||
'/npm/node/next': 'node version',
|
'/npm/node/next': 'node version',
|
||||||
'/npm/dependents/got': 'dependents'
|
'/npm/dependents/got': 'dependents'
|
||||||
}
|
},
|
||||||
}
|
handlers: {
|
||||||
|
|
||||||
export const handlers: Handlers = {
|
|
||||||
'/npm/:topic/:scope<@.+>/:pkg/:tag?': handler,
|
'/npm/:topic/:scope<@.+>/:pkg/:tag?': handler,
|
||||||
'/npm/:topic/:pkg/:tag?': handler
|
'/npm/:topic/:pkg/:tag?': handler
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default badgenServe(handlers)
|
export default createBadgenHandler(config)
|
||||||
|
|
||||||
async function handler ({ topic, scope, pkg, tag }: Args) {
|
async function handler ({ topic, scope, pkg, tag }: PathArgs) {
|
||||||
const npmName = scope ? `${scope}/${pkg}` : pkg
|
const npmName = scope ? `${scope}/${pkg}` : pkg
|
||||||
|
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
import http from 'http'
|
||||||
|
import matchRoute from 'my-way'
|
||||||
|
import urlParse from 'url-parse'
|
||||||
|
|
||||||
|
import fetchIcon from './fetch-icon'
|
||||||
|
import serveBadge from './serve-badge'
|
||||||
|
import serveDocs from './serve-docs'
|
||||||
|
import sentry from './sentry'
|
||||||
|
|
||||||
|
import { BadgenParams } from './types'
|
||||||
|
|
||||||
|
export type PathArgs = NonNullable<ReturnType<typeof matchRoute>>
|
||||||
|
|
||||||
|
export interface BadgeMaker {
|
||||||
|
(pathArgs: PathArgs) : Promise<BadgenParams>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BadgenServeConfig {
|
||||||
|
title: string;
|
||||||
|
help?: string;
|
||||||
|
examples: { [url: string]: string };
|
||||||
|
handlers: { [pattern: string]: BadgeMaker };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BadgenError {
|
||||||
|
public status: string // error badge param: status (required)
|
||||||
|
public color: string // error badge param: color
|
||||||
|
public code: number // status code for response
|
||||||
|
|
||||||
|
constructor ({ status, color = 'grey', code = 500 }) {
|
||||||
|
this.status = status
|
||||||
|
this.color = color
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBadgenHandler (conf: BadgenServeConfig): http.RequestListener {
|
||||||
|
return async function badgenHandler (req, res) {
|
||||||
|
const url = req.url ?? '/'
|
||||||
|
const { pathname, query } = urlParse(url, true)
|
||||||
|
|
||||||
|
// Serve favicon
|
||||||
|
if (pathname === '/favicon.ico') {
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match handler
|
||||||
|
let matchedArgs: ReturnType<typeof matchRoute> = null
|
||||||
|
const matchedScheme = Object.keys(conf.handlers).find(scheme => {
|
||||||
|
return matchedArgs = matchRoute(scheme, decodeURI(pathname))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Serve docs
|
||||||
|
if (!matchedScheme) {
|
||||||
|
if (matchRoute('/:name', url)) {
|
||||||
|
return serveDoc(conf)(req, res)
|
||||||
|
} else {
|
||||||
|
return serve404(req, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLabel = pathname.split('/')[1]
|
||||||
|
const defaultParams = {
|
||||||
|
subject: defaultLabel,
|
||||||
|
status: 'unknown',
|
||||||
|
color: 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve badge
|
||||||
|
try {
|
||||||
|
const badgeParamsPromise = conf.handlers[matchedScheme](matchedArgs || {})
|
||||||
|
|
||||||
|
let iconPromise: Promise<string | undefined> = Promise.resolve(undefined)
|
||||||
|
if (typeof query.icon === 'string') {
|
||||||
|
if (query.icon.startsWith('https://')) {
|
||||||
|
iconPromise = fetchIcon(query.icon).catch(e => undefined)
|
||||||
|
} else {
|
||||||
|
iconPromise = Promise.resolve(query.icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ icon, params = defaultParams ] = await Promise.all([
|
||||||
|
iconPromise,
|
||||||
|
badgeParamsPromise
|
||||||
|
])
|
||||||
|
|
||||||
|
params.subject = simpleDecode(params.subject)
|
||||||
|
params.status = simpleDecode(params.status)
|
||||||
|
|
||||||
|
if (icon !== undefined) {
|
||||||
|
query.icon = icon === '' ? params.subject : icon
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.style === undefined) {
|
||||||
|
query.style = getBadgeStyle(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serveBadge(req, res, { params, query: query as any })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadgenError) {
|
||||||
|
console.error(`BGE${error.code} "${error.status}" ${req.url}`)
|
||||||
|
return serveBadge(req, res, {
|
||||||
|
code: error.code,
|
||||||
|
sMaxAge: 5,
|
||||||
|
params: {
|
||||||
|
subject: defaultLabel,
|
||||||
|
status: error.status,
|
||||||
|
color: error.color
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle timeout for `got` requests
|
||||||
|
if (error.code === 'ETIMEDOUT') {
|
||||||
|
console.error(`APIE504 ${req.url}`)
|
||||||
|
return serveBadge(req, res, {
|
||||||
|
code: 504,
|
||||||
|
sMaxAge: 5,
|
||||||
|
params: {
|
||||||
|
subject: defaultLabel,
|
||||||
|
status: 'timeout',
|
||||||
|
color: 'grey'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle requests errors from `got`
|
||||||
|
if (error.statusCode) {
|
||||||
|
const errorInfo = `${error.url} ${error.statusMessage}`
|
||||||
|
console.error(`APIE${error.statusCode} ${url} ${errorInfo}`)
|
||||||
|
return serveBadge(req, res, {
|
||||||
|
code: 502,
|
||||||
|
sMaxAge: 5,
|
||||||
|
params: {
|
||||||
|
subject: defaultLabel,
|
||||||
|
status: error.statusCode,
|
||||||
|
color: 'grey'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sentry.configureScope((scope) => {
|
||||||
|
scope.setTag('path', url)
|
||||||
|
scope.setTag('service', defaultLabel)
|
||||||
|
})
|
||||||
|
sentry.captureException(error)
|
||||||
|
|
||||||
|
// uncatched error
|
||||||
|
console.error(`UCE ${url}`, error.message, error)
|
||||||
|
return serveBadge(req, res, {
|
||||||
|
code: 500,
|
||||||
|
sMaxAge: 5,
|
||||||
|
params: {
|
||||||
|
subject: 'badgen',
|
||||||
|
status: 'error',
|
||||||
|
color: 'grey'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBadgeStyle (req: http.IncomingMessage): string | undefined {
|
||||||
|
const host = req.headers['x-forwarded-host']?.toString() ?? req.headers.host ?? ''
|
||||||
|
return host.startsWith('flat') ? 'flat' : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function simpleDecode (str: string): string {
|
||||||
|
return String(str).replace(/%2F/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function serve404 (req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
|
const params = {
|
||||||
|
subject: 'Badgen',
|
||||||
|
status: '404',
|
||||||
|
color: 'orange'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { query } = urlParse(req.url || '/', true)
|
||||||
|
|
||||||
|
if (query.style === undefined) {
|
||||||
|
query.style = getBadgeStyle(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
serveBadge(req, res, { code: 404, params, query })
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveDoc (conf: BadgenServeConfig): http.RequestListener {
|
||||||
|
return (req, res) => {
|
||||||
|
// TODO: render docs
|
||||||
|
res.end('docs')
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { BadgenParams } from './types'
|
||||||
type ServeBadgeOptions = {
|
type ServeBadgeOptions = {
|
||||||
code?: number
|
code?: number
|
||||||
sMaxAge?: number,
|
sMaxAge?: number,
|
||||||
query?: { [key: string]: string },
|
query?: { [key: string]: string | undefined },
|
||||||
params: BadgenParams
|
params: BadgenParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ type ResolvedIcon = {
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveIcon (icon: string | undefined, width: string): ResolvedIcon {
|
function resolveIcon (icon: string | undefined, width?: string): ResolvedIcon {
|
||||||
const builtinIcon = icons[icon]
|
const builtinIcon = icons[icon]
|
||||||
if (builtinIcon) {
|
if (builtinIcon) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1265,6 +1265,12 @@
|
||||||
"integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==",
|
"integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/url-parse": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz",
|
||||||
|
@ -7807,6 +7813,11 @@
|
||||||
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"querystringify": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
@ -8097,6 +8108,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||||
},
|
},
|
||||||
|
"requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.12.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
|
||||||
|
@ -9506,6 +9522,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"url-parse": {
|
||||||
|
"version": "1.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||||
|
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||||
|
"requires": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"url-parse-lax": {
|
"url-parse-lax": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
"react-debounce-render": "^5.0.0",
|
"react-debounce-render": "^5.0.0",
|
||||||
"semver": "^6.3.0",
|
"semver": "^6.3.0",
|
||||||
"serve-handler": "^6.1.2",
|
"serve-handler": "^6.1.2",
|
||||||
"serve-marked": "^2.0.2"
|
"serve-marked": "^2.0.2",
|
||||||
|
"url-parse": "^1.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cheerio": "^0.22.13",
|
"@types/cheerio": "^0.22.13",
|
||||||
|
@ -52,6 +53,7 @@
|
||||||
"@types/react-dom": "^16.9.3",
|
"@types/react-dom": "^16.9.3",
|
||||||
"@types/semver": "^6.0.2",
|
"@types/semver": "^6.0.2",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/supertest": "^2.0.8",
|
||||||
|
"@types/url-parse": "^1.4.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.5.0",
|
"@typescript-eslint/eslint-plugin": "^2.5.0",
|
||||||
"@typescript-eslint/parser": "^2.5.0",
|
"@typescript-eslint/parser": "^2.5.0",
|
||||||
"@zeit/next-typescript": "^1.1.1",
|
"@zeit/next-typescript": "^1.1.1",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"lib": ["dom", "esnext"]
|
"lib": ["dom", "dom.iterable", "esnext"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"index.ts",
|
"index.ts",
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
Ładowanie…
Reference in New Issue