Compare commits
2 Commits
server-202
...
requires-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332a496e84 | ||
|
|
5f28ac34cc |
@@ -86,6 +86,33 @@ services_steps: &services_steps
|
|||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: junit
|
path: junit
|
||||||
|
|
||||||
|
run_package_tests: &run_package_tests
|
||||||
|
when: always
|
||||||
|
command: |
|
||||||
|
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
|
||||||
|
set +e
|
||||||
|
export NVM_DIR="/opt/circleci/.nvm"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
nvm install $NODE_VERSION
|
||||||
|
nvm use $NODE_VERSION
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# install the shields.io dependencies
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# run the package tests
|
||||||
|
npm run test:package
|
||||||
|
npm run check-types:package
|
||||||
|
|
||||||
|
# delete the sheilds.io dependencies
|
||||||
|
rm -rf node_modules/
|
||||||
|
|
||||||
|
# run a smoke test (render a badge with the CLI)
|
||||||
|
# with only the package dependencies installed
|
||||||
|
cd badge-maker
|
||||||
|
npm link
|
||||||
|
badge cactus grown :green @flat
|
||||||
|
|
||||||
package_steps: &package_steps
|
package_steps: &package_steps
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@@ -105,31 +132,31 @@ package_steps: &package_steps
|
|||||||
# https://nodejs.org/en/about/releases/
|
# https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
|
<<: *run_package_tests
|
||||||
environment:
|
environment:
|
||||||
mocha_reporter: mocha-junit-reporter
|
mocha_reporter: mocha-junit-reporter
|
||||||
MOCHA_FILE: junit/badge-maker/v10/results.xml
|
MOCHA_FILE: junit/badge-maker/v10/results.xml
|
||||||
NODE_VERSION: v10
|
NODE_VERSION: v10
|
||||||
CYPRESS_INSTALL_BINARY: 0
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
name: Run package tests on Node 10
|
name: Run package tests on Node 10
|
||||||
command: scripts/run_package_tests.sh
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
|
<<: *run_package_tests
|
||||||
environment:
|
environment:
|
||||||
mocha_reporter: mocha-junit-reporter
|
mocha_reporter: mocha-junit-reporter
|
||||||
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
||||||
NODE_VERSION: v12
|
NODE_VERSION: v12
|
||||||
CYPRESS_INSTALL_BINARY: 0
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
name: Run package tests on Node 12
|
name: Run package tests on Node 12
|
||||||
command: scripts/run_package_tests.sh
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
|
<<: *run_package_tests
|
||||||
environment:
|
environment:
|
||||||
mocha_reporter: mocha-junit-reporter
|
mocha_reporter: mocha-junit-reporter
|
||||||
MOCHA_FILE: junit/badge-maker/v14/results.xml
|
MOCHA_FILE: junit/badge-maker/v14/results.xml
|
||||||
NODE_VERSION: v14
|
NODE_VERSION: v14
|
||||||
CYPRESS_INSTALL_BINARY: 0
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
name: Run package tests on Node 14
|
name: Run package tests on Node 14
|
||||||
command: scripts/run_package_tests.sh
|
|
||||||
|
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: junit
|
path: junit
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ update_configs:
|
|||||||
- match:
|
- match:
|
||||||
dependency_name: 'eslint*'
|
dependency_name: 'eslint*'
|
||||||
update_type: 'semver:minor'
|
update_type: 'semver:minor'
|
||||||
|
- match:
|
||||||
|
dependency_name: 'enzyme*'
|
||||||
|
update_type: 'semver:minor'
|
||||||
- match:
|
- match:
|
||||||
dependency_name: 'mocha*'
|
dependency_name: 'mocha*'
|
||||||
update_type: 'semver:minor'
|
update_type: 'semver:minor'
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
extends:
|
extends:
|
||||||
- standard
|
- standard
|
||||||
- standard-jsx
|
|
||||||
- standard-react
|
- standard-react
|
||||||
- plugin:@typescript-eslint/recommended
|
- plugin:@typescript-eslint/recommended
|
||||||
- prettier
|
- prettier
|
||||||
|
|||||||
10
.github/probot.js
vendored
Normal file
10
.github/probot.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
on('pull_request.closed')
|
||||||
|
.filter(context => context.payload.pull_request.merged)
|
||||||
|
.filter(
|
||||||
|
context =>
|
||||||
|
context.payload.pull_request.head.ref.slice(0, 11) !== 'dependabot/'
|
||||||
|
)
|
||||||
|
.filter(context => context.payload.pull_request.base.ref === 'master')
|
||||||
|
.comment(`This pull request was merged to [{{ pull_request.base.ref }}]({{ repository.html_url }}/tree/{{ pull_request.base.ref }}) branch. This change is now waiting for deployment, which will usually happen within a few days. Stay tuned by joining our \`#ops\` channel on [Discord](https://discordapp.com/invite/HjJCwm5)!
|
||||||
|
|
||||||
|
After deployment, changes are copied to [gh-pages]({{ repository.html_url }}/tree/gh-pages) branch: `)
|
||||||
2
.github/workflows/auto-approve.yml
vendored
2
.github/workflows/auto-approve.yml
vendored
@@ -5,6 +5,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: chris48s/approve-bot@2.0.2
|
- uses: chris48s/approve-bot@2.0.1
|
||||||
with:
|
with:
|
||||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
|||||||
26
.github/workflows/deploy-docs.yml
vendored
26
.github/workflows/deploy-docs.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: Deploy Documentation
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.3.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build-docs
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
BRANCH: gh-pages
|
|
||||||
FOLDER: api-docs
|
|
||||||
CLEAN: true
|
|
||||||
31
.github/workflows/tag-release.yml
vendored
31
.github/workflows/tag-release.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Tag Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag-release:
|
|
||||||
if: |
|
|
||||||
github.event_name == 'pull_request' &&
|
|
||||||
github.event.action == 'closed' &&
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
contains(github.event.pull_request.labels.*.name, 'release')
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Get current date
|
|
||||||
id: date
|
|
||||||
run: echo "::set-output name=date::$(date --rfc-3339=date)"
|
|
||||||
|
|
||||||
- name: Checkout branch "master"
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: 'master'
|
|
||||||
|
|
||||||
- name: Tag Release
|
|
||||||
uses: tvdias/github-tagger@v0.0.2
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag: server-${{ steps.date.outputs.date }}
|
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
"**/*-test-helpers.js",
|
"**/*-test-helpers.js",
|
||||||
"**/*-fixtures.js",
|
"**/*-fixtures.js",
|
||||||
"**/mocha-*.js",
|
"**/mocha-*.js",
|
||||||
"**/*.test-d.ts",
|
|
||||||
"dangerfile.js",
|
"dangerfile.js",
|
||||||
"gatsby-*.js",
|
"gatsby-*.js",
|
||||||
"core/service-test-runner",
|
"core/service-test-runner",
|
||||||
|
|||||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
Note: this changelog is for the shields.io server. The changelog for the badge-maker NPM package is at https://github.com/badges/shields/blob/master/badge-maker/CHANGELOG.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## server-2021-01-18
|
|
||||||
|
|
||||||
- Gotta start somewhere
|
|
||||||
70
Makefile
Normal file
70
Makefile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
SHELL:=/bin/bash
|
||||||
|
|
||||||
|
SERVER_TMP=${TMPDIR}shields-server-deploy
|
||||||
|
FRONTEND_TMP=${TMPDIR}shields-frontend-deploy
|
||||||
|
|
||||||
|
# This branch is reserved for the deploy process and should not be used for
|
||||||
|
# development. The deploy script will clobber it. To avoid accidentally
|
||||||
|
# pushing secrets to GitHub, this branch is configured to reject pushes.
|
||||||
|
WORKING_BRANCH=server-deploy-working-branch
|
||||||
|
|
||||||
|
all: test
|
||||||
|
|
||||||
|
deploy: deploy-s0 deploy-s1 deploy-s2 clean-server-deploy deploy-gh-pages deploy-gh-pages-clean
|
||||||
|
|
||||||
|
deploy-s0: prepare-server-deploy push-s0
|
||||||
|
deploy-s1: prepare-server-deploy push-s1
|
||||||
|
deploy-s2: prepare-server-deploy push-s2
|
||||||
|
|
||||||
|
prepare-server-deploy:
|
||||||
|
# Ship a copy of the front end to each server for debugging.
|
||||||
|
# https://github.com/badges/shields/issues/1220
|
||||||
|
INCLUDE_DEV_PAGES=false \
|
||||||
|
npm run build
|
||||||
|
rm -rf ${SERVER_TMP}
|
||||||
|
git worktree prune
|
||||||
|
git worktree add -B ${WORKING_BRANCH} ${SERVER_TMP}
|
||||||
|
cp -r public ${SERVER_TMP}
|
||||||
|
git -C ${SERVER_TMP} add -f public/
|
||||||
|
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] Add frontend for debugging'
|
||||||
|
cp config/local-shields-io-production.yml ${SERVER_TMP}/config/
|
||||||
|
git -C ${SERVER_TMP} add -f config/local-shields-io-production.yml
|
||||||
|
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] MUST NOT BE ON GITHUB'
|
||||||
|
|
||||||
|
clean-server-deploy:
|
||||||
|
rm -rf ${SERVER_TMP}
|
||||||
|
git worktree prune
|
||||||
|
|
||||||
|
push-s0:
|
||||||
|
git push -f s0 ${WORKING_BRANCH}:master
|
||||||
|
|
||||||
|
push-s1:
|
||||||
|
git push -f s1 ${WORKING_BRANCH}:master
|
||||||
|
|
||||||
|
push-s2:
|
||||||
|
git push -f s2 ${WORKING_BRANCH}:master
|
||||||
|
|
||||||
|
deploy-gh-pages:
|
||||||
|
rm -rf ${FRONTEND_TMP}
|
||||||
|
git worktree prune
|
||||||
|
GATSBY_BASE_URL=https://img.shields.io \
|
||||||
|
INCLUDE_DEV_PAGES=false \
|
||||||
|
npm run build
|
||||||
|
git worktree add -B gh-pages ${FRONTEND_TMP}
|
||||||
|
git -C ${FRONTEND_TMP} ls-files | xargs git -C ${FRONTEND_TMP} rm
|
||||||
|
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Completely clean the index'
|
||||||
|
cp -r public/* ${FRONTEND_TMP}
|
||||||
|
echo shields.io > ${FRONTEND_TMP}/CNAME
|
||||||
|
touch ${FRONTEND_TMP}/.nojekyll
|
||||||
|
git -C ${FRONTEND_TMP} add .
|
||||||
|
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Add built site'
|
||||||
|
git push -f origin gh-pages
|
||||||
|
|
||||||
|
deploy-gh-pages-clean:
|
||||||
|
rm -rf ${FRONTEND_TMP}
|
||||||
|
git worktree prune
|
||||||
|
|
||||||
|
test:
|
||||||
|
npm test
|
||||||
|
|
||||||
|
.PHONY: all deploy prepare-server-deploy clean-server-deploy deploy-s0 deploy-s1 deploy-s2 push-s0 push-s1 push-s2 deploy-gh-pages deploy-gh-pages-clean deploy-heroku setup redis test
|
||||||
11
README.md
11
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/badges/shields/master/readme-logo.svg?sanitize=true"
|
<img src="https://raw.githubusercontent.com/badges/shields/master/frontend/images/logo.svg?sanitize=true"
|
||||||
height="130">
|
height="130">
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
|
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
|
||||||
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
|
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
|
||||||
alt="Total alerts"/></a>
|
alt="Total alerts"/></a>
|
||||||
|
<a href="https://github.com/badges/shields/compare/gh-pages...master">
|
||||||
|
<img src="https://img.shields.io/github/commits-since/badges/shields/gh-pages?label=commits%20to%20be%20deployed"
|
||||||
|
alt="commits to be deployed"></a>
|
||||||
<a href="https://discord.gg/HjJCwm5">
|
<a href="https://discord.gg/HjJCwm5">
|
||||||
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
|
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
@@ -83,12 +86,12 @@ and pull requests! You can peruse the [contributing guidelines][contributing].
|
|||||||
When adding or changing a service [please add tests][service-tests].
|
When adding or changing a service [please add tests][service-tests].
|
||||||
|
|
||||||
This project has quite a backlog of suggestions! If you're new to the project,
|
This project has quite a backlog of suggestions! If you're new to the project,
|
||||||
maybe you'd like to open a pull request to address one of them.
|
maybe you'd like to open a pull request to address one of them:
|
||||||
|
|
||||||
You can read a [tutorial on how to add a badge][tutorial].
|
|
||||||
|
|
||||||
[](https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
[](https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||||
|
|
||||||
|
You can read a [tutorial on how to add a badge][tutorial].
|
||||||
|
|
||||||
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
||||||
[tutorial]: doc/TUTORIAL.md
|
[tutorial]: doc/TUTORIAL.md
|
||||||
[contributing]: CONTRIBUTING.md
|
[contributing]: CONTRIBUTING.md
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 3.3.0
|
|
||||||
|
|
||||||
- Readability improvements: a dark font color is automatically used when the badge's background is too light. For example: 
|
|
||||||
- Better CSS color compliance: thanks to a switch from _is-css-color_ to _[css-color-converter](https://www.npmjs.com/package/css-color-converter)_, you can use a wider range of color formats from the latest CSS specification, for example `rgb(0 255 0)`
|
|
||||||
- Less dependencies: _badge-maker_ no longer depends on _camelcase_
|
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
- Accessibility improvements: Help users of assistive technologies to read the badges when used inline
|
- Accessibility improvements: Help users of assistive technologies to read the badges when used inline
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ function capitalize(s) {
|
|||||||
|
|
||||||
function colorsForBackground(color) {
|
function colorsForBackground(color) {
|
||||||
if (brightness(color) <= brightnessThreshold) {
|
if (brightness(color) <= brightnessThreshold) {
|
||||||
return { textColor: '#fff', shadowColor: '#010101' }
|
return {
|
||||||
} else {
|
textColor: '#fff',
|
||||||
return { textColor: '#333', shadowColor: '#ccc' }
|
shadowColor: '#010101',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
textColor: '#333',
|
||||||
|
shadowColor: '#ccc',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,12 +39,19 @@ function escapeXml(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function roundUpToOdd(val) {
|
function roundUpToOdd(val) {
|
||||||
|
// Increase chances of pixel grid alignment.
|
||||||
return val % 2 === 0 ? val + 1 : val
|
return val % 2 === 0 ? val + 1 : val
|
||||||
}
|
}
|
||||||
|
|
||||||
function preferredWidthOf(str, options) {
|
function preferredWidthOf(str) {
|
||||||
// Increase chances of pixel grid alignment.
|
return roundUpToOdd((anafanafo(str) / 10) | 0)
|
||||||
return roundUpToOdd(anafanafo(str, options) | 0)
|
}
|
||||||
|
|
||||||
|
function computeWidths({ label, message }) {
|
||||||
|
return {
|
||||||
|
labelWidth: preferredWidthOf(label),
|
||||||
|
messageWidth: preferredWidthOf(message),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAccessibleText({ label, message }) {
|
function createAccessibleText({ label, message }) {
|
||||||
@@ -77,19 +89,22 @@ function renderLogo({
|
|||||||
logoWidth = 14,
|
logoWidth = 14,
|
||||||
logoPadding = 0,
|
logoPadding = 0,
|
||||||
}) {
|
}) {
|
||||||
if (logo) {
|
if (!logo) {
|
||||||
const logoHeight = 14
|
|
||||||
const y = (badgeHeight - logoHeight) / 2
|
|
||||||
const x = horizPadding
|
|
||||||
return {
|
return {
|
||||||
hasLogo: true,
|
hasLogo: false,
|
||||||
totalLogoWidth: logoWidth + logoPadding,
|
totalLogoWidth: 0,
|
||||||
renderedLogo: `<image x="${x}" y="${y}" width="${logoWidth}" height="${logoHeight}" xlink:href="${escapeXml(
|
renderedLogo: '',
|
||||||
logo
|
|
||||||
)}"/>`,
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
return { hasLogo: false, totalLogoWidth: 0, renderedLogo: '' }
|
const logoHeight = 14
|
||||||
|
const y = (badgeHeight - logoHeight) / 2
|
||||||
|
const x = horizPadding
|
||||||
|
return {
|
||||||
|
hasLogo: true,
|
||||||
|
totalLogoWidth: logoWidth + logoPadding,
|
||||||
|
renderedLogo: `<image x="${x}" y="${y}" width="${logoWidth}" height="14" xlink:href="${escapeXml(
|
||||||
|
logo
|
||||||
|
)}"/>`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +139,7 @@ function renderText({
|
|||||||
return { renderedText: '', width: 0 }
|
return { renderedText: '', width: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const textLength = preferredWidthOf(content, { font: '11px Verdana' })
|
const textLength = preferredWidthOf(content)
|
||||||
const escapedContent = escapeXml(content)
|
const escapedContent = escapeXml(content)
|
||||||
|
|
||||||
const shadowMargin = 150 + verticalMargin
|
const shadowMargin = 150 + verticalMargin
|
||||||
@@ -176,6 +191,10 @@ function renderBadge(
|
|||||||
</svg>`
|
</svg>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripXmlWhitespace(xml) {
|
||||||
|
return xml.replace(/>\s+/g, '>').replace(/<\s+/g, '<').trim()
|
||||||
|
}
|
||||||
|
|
||||||
class Badge {
|
class Badge {
|
||||||
static get fontFamily() {
|
static get fontFamily() {
|
||||||
throw new Error('Not implemented')
|
throw new Error('Not implemented')
|
||||||
@@ -282,10 +301,6 @@ class Badge {
|
|||||||
this.renderedMessage = renderedMessage
|
this.renderedMessage = renderedMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
static render(params) {
|
|
||||||
return new this(params).render()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
throw new Error('Not implemented')
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
@@ -436,6 +451,30 @@ class FlatSquare extends Badge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function plastic(params) {
|
||||||
|
const badge = new Plastic(params)
|
||||||
|
if (params.minify) {
|
||||||
|
return stripXmlWhitespace(badge.render())
|
||||||
|
}
|
||||||
|
return badge.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function flat(params) {
|
||||||
|
const badge = new Flat(params)
|
||||||
|
if (params.minify) {
|
||||||
|
return stripXmlWhitespace(badge.render())
|
||||||
|
}
|
||||||
|
return badge.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatSquare(params) {
|
||||||
|
const badge = new FlatSquare(params)
|
||||||
|
if (params.minify) {
|
||||||
|
return stripXmlWhitespace(badge.render())
|
||||||
|
}
|
||||||
|
return badge.render()
|
||||||
|
}
|
||||||
|
|
||||||
function social({
|
function social({
|
||||||
label,
|
label,
|
||||||
message,
|
message,
|
||||||
@@ -445,6 +484,7 @@ function social({
|
|||||||
logoPadding,
|
logoPadding,
|
||||||
color = '#4c1',
|
color = '#4c1',
|
||||||
labelColor = '#555',
|
labelColor = '#555',
|
||||||
|
minify,
|
||||||
}) {
|
}) {
|
||||||
// Social label is styled with a leading capital. Convert to caps here so
|
// Social label is styled with a leading capital. Convert to caps here so
|
||||||
// width can be measured using the correct characters.
|
// width can be measured using the correct characters.
|
||||||
@@ -452,23 +492,24 @@ function social({
|
|||||||
|
|
||||||
const externalHeight = 20
|
const externalHeight = 20
|
||||||
const internalHeight = 19
|
const internalHeight = 19
|
||||||
const labelHorizPadding = 5
|
const horizPadding = 5
|
||||||
const messageHorizPadding = 4
|
|
||||||
const horizGutter = 6
|
|
||||||
const { totalLogoWidth, renderedLogo } = renderLogo({
|
const { totalLogoWidth, renderedLogo } = renderLogo({
|
||||||
logo,
|
logo,
|
||||||
badgeHeight: externalHeight,
|
badgeHeight: externalHeight,
|
||||||
horizPadding: labelHorizPadding,
|
horizPadding,
|
||||||
logoWidth,
|
logoWidth,
|
||||||
logoPadding,
|
logoPadding,
|
||||||
})
|
})
|
||||||
const hasMessage = message.length
|
const hasMessage = message.length
|
||||||
|
|
||||||
const font = 'bold 11px Helvetica'
|
let { labelWidth, messageWidth } = computeWidths({ label, message })
|
||||||
const labelTextWidth = preferredWidthOf(label, { font })
|
labelWidth += 10 + totalLogoWidth
|
||||||
const messageTextWidth = preferredWidthOf(message, { font })
|
messageWidth += 10
|
||||||
const labelRectWidth = labelTextWidth + totalLogoWidth + 2 * labelHorizPadding
|
messageWidth -= 4
|
||||||
const messageRectWidth = messageTextWidth + 2 * messageHorizPadding
|
|
||||||
|
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
||||||
|
const labelTextLength = (labelWidth - (10 + totalLogoWidth)) * 10
|
||||||
|
const escapedLabel = escapeXml(label)
|
||||||
|
|
||||||
let [leftLink, rightLink] = links
|
let [leftLink, rightLink] = links
|
||||||
leftLink = escapeXml(leftLink)
|
leftLink = escapeXml(leftLink)
|
||||||
@@ -478,35 +519,29 @@ function social({
|
|||||||
const accessibleText = createAccessibleText({ label, message })
|
const accessibleText = createAccessibleText({ label, message })
|
||||||
|
|
||||||
function renderMessageBubble() {
|
function renderMessageBubble() {
|
||||||
const messageBubbleMainX = labelRectWidth + horizGutter + 0.5
|
const messageBubbleMainX = labelWidth + 6.5
|
||||||
const messageBubbleNotchX = labelRectWidth + horizGutter
|
const messageBubbleNotchX = labelWidth + 6
|
||||||
return `
|
return `
|
||||||
<rect x="${messageBubbleMainX}" y="0.5" width="${messageRectWidth}" height="${internalHeight}" rx="2" fill="#fafafa"/>
|
<rect x="${messageBubbleMainX}" y="0.5" width="${messageWidth}" height="${internalHeight}" rx="2" fill="#fafafa"/>
|
||||||
<rect x="${messageBubbleNotchX}" y="7.5" width="0.5" height="5" stroke="#fafafa"/>
|
<rect x="${messageBubbleNotchX}" y="7.5" width="0.5" height="5" stroke="#fafafa"/>
|
||||||
<path d="M${messageBubbleMainX} 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/>
|
<path d="M${messageBubbleMainX} 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLabelText() {
|
function renderLabelText() {
|
||||||
const labelTextX =
|
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelWidth}" height="${internalHeight}" rx="2" />`
|
||||||
10 * (totalLogoWidth + labelTextWidth / 2 + labelHorizPadding)
|
|
||||||
const labelTextLength = 10 * labelTextWidth
|
|
||||||
const escapedLabel = escapeXml(label)
|
|
||||||
const shouldWrapWithLink = hasLeftLink && !shouldWrapBodyWithLink({ links })
|
|
||||||
|
|
||||||
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelRectWidth}" height="${internalHeight}" rx="2" />`
|
|
||||||
const shadow = `<text aria-hidden="true" x="${labelTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
const shadow = `<text aria-hidden="true" x="${labelTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||||
const text = `<text x="${labelTextX}" y="140" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
const text = `<text x="${labelTextX}" y="140" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||||
|
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
|
||||||
return shouldWrapWithLink
|
return `
|
||||||
? `
|
|
||||||
<a target="_blank" xlink:href="${leftLink}">
|
<a target="_blank" xlink:href="${leftLink}">
|
||||||
${shadow}
|
${shadow}
|
||||||
${text}
|
${text}
|
||||||
${rect}
|
${rect}
|
||||||
</a>
|
</a>
|
||||||
`
|
`
|
||||||
: `
|
}
|
||||||
|
return `
|
||||||
${rect}
|
${rect}
|
||||||
${shadow}
|
${shadow}
|
||||||
${text}
|
${text}
|
||||||
@@ -514,36 +549,34 @@ function social({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderMessageText() {
|
function renderMessageText() {
|
||||||
const messageTextX =
|
const messageTextX = (labelWidth + messageWidth / 2 + 6) * 10
|
||||||
10 * (labelRectWidth + horizGutter + messageRectWidth / 2)
|
const messageTextLength = (messageWidth - 8) * 10
|
||||||
const messageTextLength = 10 * messageTextWidth
|
|
||||||
const escapedMessage = escapeXml(message)
|
const escapedMessage = escapeXml(message)
|
||||||
|
const rect = `<rect width="${messageWidth + 1}" x="${
|
||||||
const rect = `<rect width="${messageRectWidth + 1}" x="${
|
labelWidth + 6
|
||||||
labelRectWidth + horizGutter
|
|
||||||
}" height="${internalHeight + 1}" fill="rgba(0,0,0,0)" />`
|
}" height="${internalHeight + 1}" fill="rgba(0,0,0,0)" />`
|
||||||
const shadow = `<text aria-hidden="true" x="${messageTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
const shadow = `<text aria-hidden="true" x="${messageTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
||||||
const text = `<text id="rlink" x="${messageTextX}" y="140" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
const text = `<text id="rlink" x="${messageTextX}" y="140" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
||||||
|
if (hasRightLink) {
|
||||||
return hasRightLink
|
return `
|
||||||
? `
|
|
||||||
<a target="_blank" xlink:href="${rightLink}">
|
<a target="_blank" xlink:href="${rightLink}">
|
||||||
${rect}
|
${rect}
|
||||||
${shadow}
|
${shadow}
|
||||||
${text}
|
${text}
|
||||||
</a>
|
</a>
|
||||||
`
|
`
|
||||||
: `
|
}
|
||||||
|
return `
|
||||||
${shadow}
|
${shadow}
|
||||||
${text}
|
${text}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderBadge(
|
const badge = renderBadge(
|
||||||
{
|
{
|
||||||
links,
|
links,
|
||||||
leftWidth: labelRectWidth + 1,
|
leftWidth: labelWidth + 1,
|
||||||
rightWidth: hasMessage ? horizGutter + messageRectWidth : 0,
|
rightWidth: hasMessage ? messageWidth + 6 : 0,
|
||||||
accessibleText,
|
accessibleText,
|
||||||
height: externalHeight,
|
height: externalHeight,
|
||||||
},
|
},
|
||||||
@@ -558,7 +591,7 @@ function social({
|
|||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<g stroke="#d5d5d5">
|
<g stroke="#d5d5d5">
|
||||||
<rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="${labelRectWidth}" height="${internalHeight}" rx="2"/>
|
<rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="${labelWidth}" height="${internalHeight}" rx="2"/>
|
||||||
${hasMessage ? renderMessageBubble() : ''}
|
${hasMessage ? renderMessageBubble() : ''}
|
||||||
</g>
|
</g>
|
||||||
${renderedLogo}
|
${renderedLogo}
|
||||||
@@ -568,6 +601,11 @@ function social({
|
|||||||
</g>
|
</g>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (minify) {
|
||||||
|
return stripXmlWhitespace(badge)
|
||||||
|
}
|
||||||
|
return badge
|
||||||
}
|
}
|
||||||
|
|
||||||
function forTheBadge({
|
function forTheBadge({
|
||||||
@@ -579,15 +617,14 @@ function forTheBadge({
|
|||||||
logoPadding,
|
logoPadding,
|
||||||
color = '#4c1',
|
color = '#4c1',
|
||||||
labelColor,
|
labelColor,
|
||||||
|
minify,
|
||||||
}) {
|
}) {
|
||||||
// For the Badge is styled in all caps. Convert to caps here so widths can
|
// For the Badge is styled in all caps. Convert to caps here so widths can
|
||||||
// be measured using the correct characters.
|
// be measured using the correct characters.
|
||||||
label = label.toUpperCase()
|
label = label.toUpperCase()
|
||||||
message = message.toUpperCase()
|
message = message.toUpperCase()
|
||||||
|
|
||||||
let labelWidth = preferredWidthOf(label, { font: '10px Verdana' }) || 0
|
let { labelWidth, messageWidth } = computeWidths({ label, message })
|
||||||
let messageWidth =
|
|
||||||
preferredWidthOf(message, { font: 'bold 10px Verdana' }) || 0
|
|
||||||
const height = 28
|
const height = 28
|
||||||
const hasLabel = label.length || labelColor
|
const hasLabel = label.length || labelColor
|
||||||
if (labelColor == null) {
|
if (labelColor == null) {
|
||||||
@@ -604,9 +641,7 @@ function forTheBadge({
|
|||||||
|
|
||||||
labelWidth += 10 + totalLogoWidth
|
labelWidth += 10 + totalLogoWidth
|
||||||
if (label.length) {
|
if (label.length) {
|
||||||
// Add 10 px of padding, plus approximately 1 px of letter spacing per
|
labelWidth += 10 + label.length * 1.5
|
||||||
// character.
|
|
||||||
labelWidth += 10 + 2 * label.length
|
|
||||||
} else if (hasLogo) {
|
} else if (hasLogo) {
|
||||||
if (hasLabel) {
|
if (hasLabel) {
|
||||||
labelWidth += 7
|
labelWidth += 7
|
||||||
@@ -617,9 +652,8 @@ function forTheBadge({
|
|||||||
labelWidth -= 11
|
labelWidth -= 11
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 20 px of padding, plus approximately 1.5 px of letter spacing per
|
messageWidth += 10
|
||||||
// character.
|
messageWidth += 10 + message.length * 2
|
||||||
messageWidth += 20 + 1.5 * message.length
|
|
||||||
const leftWidth = hasLogo && !hasLabel ? 0 : labelWidth
|
const leftWidth = hasLogo && !hasLabel ? 0 : labelWidth
|
||||||
const rightWidth =
|
const rightWidth =
|
||||||
hasLogo && !hasLabel ? messageWidth + labelWidth : messageWidth
|
hasLogo && !hasLabel ? messageWidth + labelWidth : messageWidth
|
||||||
@@ -641,9 +675,7 @@ function forTheBadge({
|
|||||||
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
||||||
const labelTextLength = (labelWidth - (24 + totalLogoWidth)) * 10
|
const labelTextLength = (labelWidth - (24 + totalLogoWidth)) * 10
|
||||||
const escapedLabel = escapeXml(label)
|
const escapedLabel = escapeXml(label)
|
||||||
|
|
||||||
const text = `<text fill="${textColor}" x="${labelTextX}" y="175" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
const text = `<text fill="${textColor}" x="${labelTextX}" y="175" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||||
|
|
||||||
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
|
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
|
||||||
return `
|
return `
|
||||||
<a target="_blank" xlink:href="${leftLink}">
|
<a target="_blank" xlink:href="${leftLink}">
|
||||||
@@ -651,21 +683,18 @@ function forTheBadge({
|
|||||||
${text}
|
${text}
|
||||||
</a>
|
</a>
|
||||||
`
|
`
|
||||||
} else {
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessageText() {
|
function renderMessageText() {
|
||||||
const { textColor } = colorsForBackground(color)
|
const { textColor } = colorsForBackground(color)
|
||||||
|
|
||||||
const text = `<text fill="${textColor}" x="${
|
const text = `<text fill="${textColor}" x="${
|
||||||
(labelWidth + messageWidth / 2) * 10
|
(labelWidth + messageWidth / 2) * 10
|
||||||
}" y="175" font-weight="bold" transform="scale(.1)" textLength="${
|
}" y="175" font-weight="bold" transform="scale(.1)" textLength="${
|
||||||
(messageWidth - 24) * 10
|
(messageWidth - 24) * 10
|
||||||
}">
|
}">
|
||||||
${escapeXml(message)}</text>`
|
${escapeXml(message)}</text>`
|
||||||
|
|
||||||
if (hasRightLink) {
|
if (hasRightLink) {
|
||||||
return `
|
return `
|
||||||
<a target="_blank" xlink:href="${rightLink}">
|
<a target="_blank" xlink:href="${rightLink}">
|
||||||
@@ -673,12 +702,11 @@ function forTheBadge({
|
|||||||
${text}
|
${text}
|
||||||
</a>
|
</a>
|
||||||
`
|
`
|
||||||
} else {
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderBadge(
|
const badge = renderBadge(
|
||||||
{
|
{
|
||||||
links,
|
links,
|
||||||
leftWidth,
|
leftWidth,
|
||||||
@@ -697,12 +725,17 @@ function forTheBadge({
|
|||||||
${renderMessageText()}
|
${renderMessageText()}
|
||||||
</g>`
|
</g>`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (minify) {
|
||||||
|
return stripXmlWhitespace(badge)
|
||||||
|
}
|
||||||
|
return badge
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plastic: params => Plastic.render(params),
|
plastic,
|
||||||
flat: params => Flat.render(params),
|
flat,
|
||||||
'flat-square': params => FlatSquare.render(params),
|
|
||||||
social,
|
social,
|
||||||
|
'flat-square': flatSquare,
|
||||||
'for-the-badge': forTheBadge,
|
'for-the-badge': forTheBadge,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { fromString } = require('css-color-converter')
|
const cssColorConverter = require('css-color-converter')
|
||||||
|
|
||||||
// When updating these, be sure also to update the list in `badge-maker/README.md`.
|
// When updating these, be sure also to update the list in `badge-maker/README.md`.
|
||||||
const namedColors = {
|
const namedColors = {
|
||||||
@@ -38,7 +38,10 @@ function isHexColor(s = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCSSColor(color) {
|
function isCSSColor(color) {
|
||||||
return typeof color === 'string' && fromString(color.trim())
|
return (
|
||||||
|
typeof color === 'string' &&
|
||||||
|
typeof cssColorConverter(color.trim()).toRgbaArray() !== 'undefined'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeColor(color) {
|
function normalizeColor(color) {
|
||||||
@@ -70,9 +73,8 @@ function toSvgColor(color) {
|
|||||||
|
|
||||||
function brightness(color) {
|
function brightness(color) {
|
||||||
if (color) {
|
if (color) {
|
||||||
const cssColor = fromString(color)
|
const rgb = cssColorConverter(color).toRgbaArray()
|
||||||
if (cssColor) {
|
if (rgb) {
|
||||||
const rgb = cssColor.toRgbaArray()
|
|
||||||
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2)
|
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ test(normalizeColor, () => {
|
|||||||
given(' blue ').expect(' blue ')
|
given(' blue ').expect(' blue ')
|
||||||
given('rgb(100%, 200%, 222%)').expect('rgb(100%, 200%, 222%)')
|
given('rgb(100%, 200%, 222%)').expect('rgb(100%, 200%, 222%)')
|
||||||
given('rgb(122, 200, 222)').expect('rgb(122, 200, 222)')
|
given('rgb(122, 200, 222)').expect('rgb(122, 200, 222)')
|
||||||
given('rgb(122, 200, 222, 1)').expect('rgb(122, 200, 222, 1)')
|
given('rgb(100%, 200, 222)').expect('rgb(100%, 200, 222)')
|
||||||
given('rgba(100, 20, 111, 1)').expect('rgba(100, 20, 111, 1)')
|
given('rgba(100, 20, 111, 1)').expect('rgba(100, 20, 111, 1)')
|
||||||
given('hsl(122, 200%, 222%)').expect('hsl(122, 200%, 222%)')
|
given('hsl(122, 200%, 222%)').expect('hsl(122, 200%, 222%)')
|
||||||
given('hsla(122, 200%, 222%, 1)').expect('hsla(122, 200%, 222%, 1)')
|
given('hsla(122, 200%, 222%, 1)').expect('hsla(122, 200%, 222%, 1)')
|
||||||
@@ -46,8 +46,8 @@ test(normalizeColor, () => {
|
|||||||
given(''),
|
given(''),
|
||||||
given('not-a-color'),
|
given('not-a-color'),
|
||||||
given('#ABCFGH'),
|
given('#ABCFGH'),
|
||||||
|
given('rgb(122, 200, 222, 1)'),
|
||||||
given('rgb(-100, 20, 111)'),
|
given('rgb(-100, 20, 111)'),
|
||||||
given('rgb(100%, 200, 222)'),
|
|
||||||
given('rgba(-100, 20, 111, 1.1)'),
|
given('rgba(-100, 20, 111, 1.1)'),
|
||||||
given('hsl(122, 200, 222, 1)'),
|
given('hsl(122, 200, 222, 1)'),
|
||||||
given('hsl(122, 200, 222)'),
|
given('hsl(122, 200, 222)'),
|
||||||
|
|||||||
@@ -51,8 +51,14 @@ function _clean(format) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Legacy.
|
// convert "public" format to "internal" format
|
||||||
cleaned.label = cleaned.label || ''
|
cleaned.text = [cleaned.label || '', cleaned.message]
|
||||||
|
delete cleaned.label
|
||||||
|
delete cleaned.message
|
||||||
|
if ('style' in cleaned) {
|
||||||
|
cleaned.template = cleaned.style
|
||||||
|
delete cleaned.style
|
||||||
|
}
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,14 @@
|
|||||||
const { normalizeColor, toSvgColor } = require('./color')
|
const { normalizeColor, toSvgColor } = require('./color')
|
||||||
const badgeRenderers = require('./badge-renderers')
|
const badgeRenderers = require('./badge-renderers')
|
||||||
|
|
||||||
function stripXmlWhitespace(xml) {
|
|
||||||
return xml.replace(/>\s+/g, '>').replace(/<\s+/g, '<').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
||||||
it is likely this will impact on the package's public interface in index.js
|
it is likely this will impact on the package's public interface in index.js
|
||||||
*/
|
*/
|
||||||
module.exports = function makeBadge({
|
module.exports = function makeBadge({
|
||||||
format,
|
format,
|
||||||
style = 'flat',
|
template = 'flat',
|
||||||
label,
|
text,
|
||||||
message,
|
|
||||||
color,
|
color,
|
||||||
labelColor,
|
labelColor,
|
||||||
logo,
|
logo,
|
||||||
@@ -24,8 +19,9 @@ module.exports = function makeBadge({
|
|||||||
links = ['', ''],
|
links = ['', ''],
|
||||||
}) {
|
}) {
|
||||||
// String coercion and whitespace removal.
|
// String coercion and whitespace removal.
|
||||||
label = `${label}`.trim()
|
text = text.map(value => `${value}`.trim())
|
||||||
message = `${message}`.trim()
|
|
||||||
|
const [label, message] = text
|
||||||
|
|
||||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||||
if (format === 'json') {
|
if (format === 'json') {
|
||||||
@@ -43,24 +39,23 @@ module.exports = function makeBadge({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const render = badgeRenderers[style]
|
const render = badgeRenderers[template]
|
||||||
if (!render) {
|
if (!render) {
|
||||||
throw new Error(`Unknown badge style: '${style}'`)
|
throw new Error(`Unknown template: '${template}'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logoWidth = +logoWidth || (logo ? 14 : 0)
|
logoWidth = +logoWidth || (logo ? 14 : 0)
|
||||||
|
|
||||||
return stripXmlWhitespace(
|
return render({
|
||||||
render({
|
label,
|
||||||
label,
|
message,
|
||||||
message,
|
links,
|
||||||
links,
|
logo,
|
||||||
logo,
|
logoPosition,
|
||||||
logoPosition,
|
logoWidth,
|
||||||
logoWidth,
|
logoPadding: logo && label.length ? 3 : 0,
|
||||||
logoPadding: logo && label.length ? 3 : 0,
|
color: toSvgColor(color),
|
||||||
color: toSvgColor(color),
|
labelColor: toSvgColor(labelColor),
|
||||||
labelColor: toSvgColor(labelColor),
|
minify: true,
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ const { test, given, forCases } = require('sazerac')
|
|||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const snapshot = require('snap-shot-it')
|
const snapshot = require('snap-shot-it')
|
||||||
const isSvg = require('is-svg')
|
const isSvg = require('is-svg')
|
||||||
const prettier = require('prettier')
|
|
||||||
const makeBadge = require('./make-badge')
|
const makeBadge = require('./make-badge')
|
||||||
|
|
||||||
function expectBadgeToMatchSnapshot(format) {
|
|
||||||
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function testColor(color = '', colorAttr = 'color') {
|
function testColor(color = '', colorAttr = 'color') {
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
makeBadge({
|
makeBadge({
|
||||||
label: 'name',
|
text: ['name', 'Bob'],
|
||||||
message: 'Bob',
|
|
||||||
[colorAttr]: color,
|
[colorAttr]: color,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
})
|
})
|
||||||
@@ -40,14 +34,10 @@ describe('The badge generator', function () {
|
|||||||
]).expect('#abc123')
|
]).expect('#abc123')
|
||||||
// valid rgb(a)
|
// valid rgb(a)
|
||||||
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
||||||
given('rgb(220,128,255,0.5)').expect('rgb(220,128,255,0.5)')
|
|
||||||
given('rgba(0,0,255)').expect('rgba(0,0,255)')
|
|
||||||
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
||||||
// valid hsl(a)
|
// valid hsl(a)
|
||||||
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
||||||
given('hsl(360,50%,50%,0.5)').expect('hsl(360,50%,50%,0.5)')
|
|
||||||
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
||||||
given('hsla(0,50%,101%)').expect('hsla(0,50%,101%)')
|
|
||||||
// CSS named color.
|
// CSS named color.
|
||||||
given('papayawhip').expect('papayawhip')
|
given('papayawhip').expect('papayawhip')
|
||||||
// Shields named color.
|
// Shields named color.
|
||||||
@@ -63,6 +53,12 @@ describe('The badge generator', function () {
|
|||||||
// invalid hex
|
// invalid hex
|
||||||
given('#123red'), // contains letter above F
|
given('#123red'), // contains letter above F
|
||||||
given('#red'), // contains letter above F
|
given('#red'), // contains letter above F
|
||||||
|
// invalid rgb(a)
|
||||||
|
given('rgb(220,128,255,0.5)'), // has alpha
|
||||||
|
given('rgba(0,0,255)'), // no alpha
|
||||||
|
// invalid hsl(a)
|
||||||
|
given('hsl(360,50%,50%,0.5)'), // has alpha
|
||||||
|
given('hsla(0,50%,101%)'), // no alpha
|
||||||
// neither a css named color nor colorscheme
|
// neither a css named color nor colorscheme
|
||||||
given('notacolor'),
|
given('notacolor'),
|
||||||
given('bluish'),
|
given('bluish'),
|
||||||
@@ -81,26 +77,23 @@ describe('The badge generator', function () {
|
|||||||
|
|
||||||
describe('SVG', function () {
|
describe('SVG', function () {
|
||||||
it('should produce SVG', function () {
|
it('should produce SVG', function () {
|
||||||
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
|
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||||
|
expect(svg)
|
||||||
.to.satisfy(isSvg)
|
.to.satisfy(isSvg)
|
||||||
.and.to.include('cactus')
|
.and.to.include('cactus')
|
||||||
.and.to.include('grown')
|
.and.to.include('grown')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshot', function () {
|
it('should match snapshot', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||||
label: 'cactus',
|
snapshot(svg)
|
||||||
message: 'grown',
|
|
||||||
format: 'svg',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('JSON', function () {
|
describe('JSON', function () {
|
||||||
it('should produce the expected JSON', function () {
|
it('should produce the expected JSON', function () {
|
||||||
const json = makeBadge({
|
const json = makeBadge({
|
||||||
label: 'cactus',
|
text: ['cactus', 'grown'],
|
||||||
message: 'grown',
|
|
||||||
format: 'json',
|
format: 'json',
|
||||||
links: ['https://example.com/', 'https://other.example.com/'],
|
links: ['https://example.com/', 'https://other.example.com/'],
|
||||||
})
|
})
|
||||||
@@ -113,471 +106,484 @@ describe('The badge generator', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should replace undefined svg badge style with "flat"', function () {
|
it('should replace undefined svg template with "flat"', function () {
|
||||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||||
label: 'name',
|
text: ['name', 'Bob'],
|
||||||
message: 'Bob',
|
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
})
|
})
|
||||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||||
label: 'name',
|
text: ['name', 'Bob'],
|
||||||
message: 'Bob',
|
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
})
|
})
|
||||||
expect(jsonBadgeWithUnknownStyle)
|
expect(jsonBadgeWithUnknownStyle)
|
||||||
.to.equal(jsonBadgeWithDefaultStyle)
|
.to.equal(jsonBadgeWithDefaultStyle)
|
||||||
.and.to.satisfy(isSvg)
|
.and.to.satisfy(isSvg)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fail with unknown svg badge style', function () {
|
it('should fail with unknown svg template', function () {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
makeBadge({
|
makeBadge({
|
||||||
label: 'name',
|
text: ['name', 'Bob'],
|
||||||
message: 'Bob',
|
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'unknown_style',
|
template: 'unknown_style',
|
||||||
})
|
})
|
||||||
).to.throw(Error, "Unknown badge style: 'unknown_style'")
|
).to.throw(Error, "Unknown template: 'unknown_style'")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"flat" template badge generation', function () {
|
describe('"flat" template badge generation', function () {
|
||||||
it('should match snapshots: message/label, no logo', function () {
|
it('should match snapshots: message/label, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with logo', function () {
|
it('should match snapshots: message/label, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, no logo', function () {
|
it('should match snapshots: message only, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo', function () {
|
it('should match snapshots: message only, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with links', function () {
|
it('should match snapshots: message/label, with links', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"flat-square" template badge generation', function () {
|
describe('"flat-square" template badge generation', function () {
|
||||||
it('should match snapshots: message/label, no logo', function () {
|
it('should match snapshots: message/label, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with logo', function () {
|
it('should match snapshots: message/label, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, no logo', function () {
|
it('should match snapshots: message only, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo', function () {
|
it('should match snapshots: message only, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with links', function () {
|
it('should match snapshots: message/label, with links', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat-square',
|
template: 'flat-square',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"plastic" template badge generation', function () {
|
describe('"plastic" template badge generation', function () {
|
||||||
it('should match snapshots: message/label, no logo', function () {
|
it('should match snapshots: message/label, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with logo', function () {
|
it('should match snapshots: message/label, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, no logo', function () {
|
it('should match snapshots: message only, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo', function () {
|
it('should match snapshots: message only, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with links', function () {
|
it('should match snapshots: message/label, with links', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'plastic',
|
template: 'plastic',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"for-the-badge" template badge generation', function () {
|
describe('"for-the-badge" template badge generation', function () {
|
||||||
// https://github.com/badges/shields/issues/1280
|
// https://github.com/badges/shields/issues/1280
|
||||||
it('numbers should produce a string', function () {
|
it('numbers should produce a string', function () {
|
||||||
expect(
|
const svg = makeBadge({
|
||||||
makeBadge({
|
text: [1998, 1999],
|
||||||
label: 1998,
|
format: 'svg',
|
||||||
message: 1999,
|
template: 'for-the-badge',
|
||||||
format: 'svg',
|
})
|
||||||
style: 'for-the-badge',
|
expect(svg).to.include('1998').and.to.include('1999')
|
||||||
})
|
|
||||||
)
|
|
||||||
.to.include('1998')
|
|
||||||
.and.to.include('1999')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lowercase/mixedcase string should produce uppercase string', function () {
|
it('lowercase/mixedcase string should produce uppercase string', function () {
|
||||||
expect(
|
const svg = makeBadge({
|
||||||
makeBadge({
|
text: ['Label', '1 string'],
|
||||||
label: 'Label',
|
format: 'svg',
|
||||||
message: '1 string',
|
template: 'for-the-badge',
|
||||||
format: 'svg',
|
})
|
||||||
style: 'for-the-badge',
|
expect(svg).to.include('LABEL').and.to.include('1 STRING')
|
||||||
})
|
|
||||||
)
|
|
||||||
.to.include('LABEL')
|
|
||||||
.and.to.include('1 STRING')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, no logo', function () {
|
it('should match snapshots: message/label, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with logo', function () {
|
it('should match snapshots: message/label, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, no logo', function () {
|
it('should match snapshots: message only, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo', function () {
|
it('should match snapshots: message only, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with links', function () {
|
it('should match snapshots: message/label, with links', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('"social" template badge generation', function () {
|
describe('"social" template badge generation', function () {
|
||||||
it('should produce capitalized string for badge key', function () {
|
it('should produce capitalized string for badge key', function () {
|
||||||
expect(
|
const svg = makeBadge({
|
||||||
makeBadge({
|
text: ['some-key', 'some-value'],
|
||||||
label: 'some-key',
|
format: 'svg',
|
||||||
message: 'some-value',
|
template: 'social',
|
||||||
format: 'svg',
|
})
|
||||||
style: 'social',
|
expect(svg).to.include('Some-key').and.to.include('some-value')
|
||||||
})
|
|
||||||
)
|
|
||||||
.to.include('Some-key')
|
|
||||||
.and.to.include('some-value')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// https://github.com/badges/shields/issues/1606
|
// https://github.com/badges/shields/issues/1606
|
||||||
it('should handle empty strings used as badge keys', function () {
|
it('should handle empty strings used as badge keys', function () {
|
||||||
expect(
|
const svg = makeBadge({
|
||||||
makeBadge({
|
text: ['', 'some-value'],
|
||||||
label: '',
|
format: 'json',
|
||||||
message: 'some-value',
|
template: 'social',
|
||||||
format: 'json',
|
})
|
||||||
style: 'social',
|
expect(svg).to.include('""').and.to.include('some-value')
|
||||||
})
|
|
||||||
)
|
|
||||||
.to.include('""')
|
|
||||||
.and.to.include('some-value')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, no logo', function () {
|
it('should match snapshots: message/label, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with logo', function () {
|
it('should match snapshots: message/label, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, no logo', function () {
|
it('should match snapshots: message only, no logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo', function () {
|
it('should match snapshots: message only, with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: '',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match snapshots: message/label, with links', function () {
|
it('should match snapshots: message/label, with links', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'social',
|
template: 'social',
|
||||||
color: '#b3e',
|
color: '#b3e',
|
||||||
labelColor: '#0f0',
|
labelColor: '#0f0',
|
||||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('badges with logos should always produce the same badge', function () {
|
describe('badges with logos should always produce the same badge', function () {
|
||||||
it('badge with logo', function () {
|
it('badge with logo', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
const svg = makeBadge({
|
||||||
label: 'label',
|
text: ['label', 'message'],
|
||||||
message: 'message',
|
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||||
})
|
})
|
||||||
|
snapshot(svg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('text colors', function () {
|
describe('text colors', function () {
|
||||||
it('should use black text when the label color is light', function () {
|
it('should use black text when the label color is light', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
color: '#000',
|
color: '#000',
|
||||||
labelColor: '#f3f3f3',
|
labelColor: '#f3f3f3',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use black text when the message color is light', function () {
|
it('should use black text when the message color is light', function () {
|
||||||
expectBadgeToMatchSnapshot({
|
snapshot(
|
||||||
label: 'cactus',
|
makeBadge({
|
||||||
message: 'grown',
|
text: ['cactus', 'grown'],
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
style: 'for-the-badge',
|
template: 'for-the-badge',
|
||||||
color: '#e2ffe1',
|
color: '#e2ffe1',
|
||||||
labelColor: '#000',
|
labelColor: '#000',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "badge-maker",
|
"name": "badge-maker",
|
||||||
"version": "3.3.0",
|
"version": "3.2.0",
|
||||||
"description": "Shields.io badge library",
|
"description": "Shields.io badge library",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"GitHub",
|
"GitHub",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"logo": "https://opencollective.com/opencollective/logo.txt"
|
"logo": "https://opencollective.com/opencollective/logo.txt"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anafanafo": "2.0.0",
|
"anafanafo": "^1.0.0",
|
||||||
"css-color-converter": "^2.0.0"
|
"css-color-converter": "^1.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo 'Run tests from parent dir'; false"
|
"test": "echo 'Run tests from parent dir'; false"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public:
|
|||||||
key: 'HTTPS_KEY'
|
key: 'HTTPS_KEY'
|
||||||
cert: 'HTTPS_CRT'
|
cert: 'HTTPS_CRT'
|
||||||
|
|
||||||
redirectUrl: 'REDIRECT_URI'
|
redirectUri: 'REDIRECT_URI'
|
||||||
|
|
||||||
rasterUrl: 'RASTER_URL'
|
rasterUrl: 'RASTER_URL'
|
||||||
|
|
||||||
@@ -30,6 +30,9 @@ public:
|
|||||||
__name: 'ALLOWED_ORIGIN'
|
__name: 'ALLOWED_ORIGIN'
|
||||||
__format: 'json'
|
__format: 'json'
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
dir: 'PERSISTENCE_DIR'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bitbucketServer:
|
bitbucketServer:
|
||||||
authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
|
authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
|
||||||
@@ -61,8 +64,6 @@ public:
|
|||||||
|
|
||||||
fetchLimit: 'FETCH_LIMIT'
|
fetchLimit: 'FETCH_LIMIT'
|
||||||
|
|
||||||
requireCloudflare: 'REQUIRE_CLOUDFLARE'
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
|
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
|
||||||
bintray_user: 'BINTRAY_USER'
|
bintray_user: 'BINTRAY_USER'
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public:
|
|||||||
cors:
|
cors:
|
||||||
allowedOrigin: []
|
allowedOrigin: []
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
dir: './private'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
github:
|
github:
|
||||||
baseUri: 'https://api.github.com/'
|
baseUri: 'https://api.github.com/'
|
||||||
@@ -33,6 +36,4 @@ public:
|
|||||||
|
|
||||||
fetchLimit: '10MB'
|
fetchLimit: '10MB'
|
||||||
|
|
||||||
requireCloudflare: false
|
|
||||||
|
|
||||||
private: {}
|
private: {}
|
||||||
|
|||||||
@@ -15,4 +15,10 @@ public:
|
|||||||
cors:
|
cors:
|
||||||
allowedOrigin: ['http://shields.io', 'https://shields.io']
|
allowedOrigin: ['http://shields.io', 'https://shields.io']
|
||||||
|
|
||||||
|
redirectUrl: 'https://shields.io/'
|
||||||
|
|
||||||
rasterUrl: 'https://raster.shields.io'
|
rasterUrl: 'https://raster.shields.io'
|
||||||
|
|
||||||
|
private:
|
||||||
|
# These are not really private; they should be moved to `public`.
|
||||||
|
shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133']
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
function escapeFormat(t) {
|
function escapeFormat(t) {
|
||||||
return (
|
return (
|
||||||
t
|
t
|
||||||
// Single underscore.
|
// Inline single underscore.
|
||||||
.replace(/(^|[^_])((?:__)*)_(?!_)/g, '$1$2 ')
|
.replace(/([^_])_([^_])/g, '$1 $2')
|
||||||
|
// Leading or trailing underscore.
|
||||||
|
.replace(/([^_])_$/, '$1 ')
|
||||||
|
.replace(/^_([^_])/, ' $1')
|
||||||
// Double underscore and double dash.
|
// Double underscore and double dash.
|
||||||
.replace(/__/g, '_')
|
.replace(/__/g, '_')
|
||||||
.replace(/--/g, '-')
|
.replace(/--/g, '-')
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const { test, given } = require('sazerac')
|
|
||||||
const { escapeFormat } = require('./path-helpers')
|
|
||||||
|
|
||||||
describe('Badge URL helper functions', function () {
|
|
||||||
test(escapeFormat, () => {
|
|
||||||
given('_single leading underscore').expect(' single leading underscore')
|
|
||||||
given('single trailing underscore_').expect('single trailing underscore ')
|
|
||||||
given('__double leading underscores').expect('_double leading underscores')
|
|
||||||
given('double trailing underscores__').expect(
|
|
||||||
'double trailing underscores_'
|
|
||||||
)
|
|
||||||
given('treble___underscores').expect('treble_ underscores')
|
|
||||||
given('fourfold____underscores').expect('fourfold__underscores')
|
|
||||||
given('double--dashes').expect('double-dashes')
|
|
||||||
given('treble---dashes').expect('treble--dashes')
|
|
||||||
given('fourfold----dashes').expect('fourfold--dashes')
|
|
||||||
given('once_in_a_blue--moon').expect('once in a blue-moon')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const gql = require('graphql-tag')
|
const gql = require('graphql-tag')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const BaseJsonService = require('./base-json')
|
const BaseJsonService = require('./base-json')
|
||||||
|
|||||||
74
core/base-service/base-non-memory-caching.js
Normal file
74
core/base-service/base-non-memory-caching.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||||
|
const BaseService = require('./base')
|
||||||
|
const { MetricHelper } = require('./metric-helper')
|
||||||
|
const { setCacheHeaders } = require('./cache-headers')
|
||||||
|
const { makeSend } = require('./legacy-result-sender')
|
||||||
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
|
const { prepareRoute, namedParamsForMatch } = require('./route')
|
||||||
|
|
||||||
|
// Badges are subject to two independent types of caching: in-memory and
|
||||||
|
// downstream.
|
||||||
|
//
|
||||||
|
// Services deriving from `NonMemoryCachingBaseService` are not cached in
|
||||||
|
// memory on the server. This means that each request that hits the server
|
||||||
|
// triggers another call to the handler. When using badges for server
|
||||||
|
// diagnostics, that's useful!
|
||||||
|
//
|
||||||
|
// In contrast, The `handle()` function of most other `BaseService`
|
||||||
|
// subclasses is wrapped in onboard, in-memory caching. See `lib /request-
|
||||||
|
// handler.js` and `BaseService.prototype.register()`.
|
||||||
|
//
|
||||||
|
// All services, including those extending NonMemoryCachingBaseServices, may
|
||||||
|
// be cached _downstream_. This is governed by cache headers, which are
|
||||||
|
// configured by the service, the user's request, and the server's default
|
||||||
|
// cache length.
|
||||||
|
module.exports = class NonMemoryCachingBaseService extends BaseService {
|
||||||
|
static register({ camp, metricInstance }, serviceConfig) {
|
||||||
|
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||||
|
const { _cacheLength: serviceDefaultCacheLengthSeconds } = this
|
||||||
|
const { regex, captureNames } = prepareRoute(this.route)
|
||||||
|
|
||||||
|
const metricHelper = MetricHelper.create({
|
||||||
|
metricInstance,
|
||||||
|
ServiceClass: this,
|
||||||
|
})
|
||||||
|
|
||||||
|
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||||
|
const metricHandle = metricHelper.startRequest()
|
||||||
|
|
||||||
|
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||||
|
const serviceData = await this.invoke(
|
||||||
|
{},
|
||||||
|
serviceConfig,
|
||||||
|
namedParams,
|
||||||
|
queryParams
|
||||||
|
)
|
||||||
|
|
||||||
|
const badgeData = coalesceBadge(
|
||||||
|
queryParams,
|
||||||
|
serviceData,
|
||||||
|
this.defaultBadgeData,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
|
||||||
|
// The final capture group is the extension.
|
||||||
|
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||||
|
badgeData.format = format
|
||||||
|
|
||||||
|
const svg = makeBadge(badgeData)
|
||||||
|
|
||||||
|
setCacheHeaders({
|
||||||
|
cacheHeaderConfig,
|
||||||
|
serviceDefaultCacheLengthSeconds,
|
||||||
|
queryParams,
|
||||||
|
res: ask.res,
|
||||||
|
})
|
||||||
|
|
||||||
|
makeSend(format, ask.res, end)(svg)
|
||||||
|
|
||||||
|
metricHandle.noteResponseSent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||||
const BaseSvgScrapingService = require('./base-svg-scraping')
|
const BaseSvgScrapingService = require('./base-svg-scraping')
|
||||||
|
|
||||||
|
function makeExampleSvg({ label, message }) {
|
||||||
|
return makeBadge({ text: ['this is the label', 'this is the result!'] })
|
||||||
|
}
|
||||||
|
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
message: Joi.string().required(),
|
message: Joi.string().required(),
|
||||||
}).required()
|
}).required()
|
||||||
@@ -25,7 +29,10 @@ class DummySvgScrapingService extends BaseSvgScrapingService {
|
|||||||
describe('BaseSvgScrapingService', function () {
|
describe('BaseSvgScrapingService', function () {
|
||||||
const exampleLabel = 'this is the label'
|
const exampleLabel = 'this is the label'
|
||||||
const exampleMessage = 'this is the result!'
|
const exampleMessage = 'this is the result!'
|
||||||
const exampleSvg = makeBadge({ label: exampleLabel, message: exampleMessage })
|
const exampleSvg = makeExampleSvg({
|
||||||
|
label: exampleLabel,
|
||||||
|
message: exampleMessage,
|
||||||
|
})
|
||||||
|
|
||||||
describe('valueFromSvgBadge', function () {
|
describe('valueFromSvgBadge', function () {
|
||||||
it('should find the correct value', function () {
|
it('should find the correct value', function () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const BaseXmlService = require('./base-xml')
|
const BaseXmlService = require('./base-xml')
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class BaseYamlService extends BaseService {
|
|||||||
})
|
})
|
||||||
let parsed
|
let parsed
|
||||||
try {
|
try {
|
||||||
parsed = yaml.load(buffer.toString(), encoding)
|
parsed = yaml.safeLoad(buffer.toString(), encoding)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logTrace(emojic.dart, 'Response YAML (unparseable)', buffer)
|
logTrace(emojic.dart, 'Response YAML (unparseable)', buffer)
|
||||||
throw new InvalidResponse({
|
throw new InvalidResponse({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const BaseYamlService = require('./base-yaml')
|
const BaseYamlService = require('./base-yaml')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// See available emoji at http://emoji.muan.co/
|
// See available emoji at http://emoji.muan.co/
|
||||||
const emojic = require('emojic')
|
const emojic = require('emojic')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const log = require('../server/log')
|
const log = require('../server/log')
|
||||||
const { AuthHelper } = require('./auth-helper')
|
const { AuthHelper } = require('./auth-helper')
|
||||||
const { MetricHelper, MetricNames } = require('./metric-helper')
|
const { MetricHelper, MetricNames } = require('./metric-helper')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const { expect } = chai
|
const { expect } = chai
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
@@ -329,7 +329,7 @@ describe('BaseService', function () {
|
|||||||
describe('ScoutCamp integration', function () {
|
describe('ScoutCamp integration', function () {
|
||||||
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
|
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/
|
const expectedRouteRegex = /^\/foo\/([^\/]+?)(|\.svg|\.json)$/
|
||||||
|
|
||||||
let mockCamp
|
let mockCamp
|
||||||
let mockHandleRequest
|
let mockHandleRequest
|
||||||
@@ -373,10 +373,9 @@ describe('BaseService', function () {
|
|||||||
const expectedFormat = 'svg'
|
const expectedFormat = 'svg'
|
||||||
expect(mockSendBadge).to.have.been.calledOnce
|
expect(mockSendBadge).to.have.been.calledOnce
|
||||||
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
|
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
|
||||||
label: 'cat',
|
text: ['cat', 'Hello namedParamA: bar with queryParamA: ?'],
|
||||||
message: 'Hello namedParamA: bar with queryParamA: ?',
|
|
||||||
color: 'lightgrey',
|
color: 'lightgrey',
|
||||||
style: 'flat',
|
template: 'flat',
|
||||||
namedLogo: undefined,
|
namedLogo: undefined,
|
||||||
logo: undefined,
|
logo: undefined,
|
||||||
logoWidth: undefined,
|
logoWidth: undefined,
|
||||||
@@ -519,7 +518,7 @@ describe('BaseService', function () {
|
|||||||
|
|
||||||
await serviceInstance._request({ url })
|
await serviceInstance._request({ url })
|
||||||
|
|
||||||
expect(await register.getSingleMetricAsString('service_response_bytes'))
|
expect(register.getSingleMetricAsString('service_response_bytes'))
|
||||||
.to.contain(
|
.to.contain(
|
||||||
'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n'
|
'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n'
|
||||||
)
|
)
|
||||||
@@ -545,7 +544,7 @@ describe('BaseService', function () {
|
|||||||
await serviceInstance._request({ url })
|
await serviceInstance._request({ url })
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await register.getSingleMetricAsString('service_response_bytes')
|
register.getSingleMetricAsString('service_response_bytes')
|
||||||
).to.not.contain('service_response_bytes_bucket')
|
).to.not.contain('service_response_bytes_bucket')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const coalesce = require('./coalesce')
|
const coalesce = require('./coalesce')
|
||||||
|
|
||||||
const serverStartTimeGMTString = new Date().toGMTString()
|
const serverStartTimeGMTString = new Date().toGMTString()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const categories = require('../../services/categories')
|
const categories = require('../../services/categories')
|
||||||
|
|
||||||
const isRealCategory = Joi.equal(...categories.map(({ id }) => id)).required()
|
const isRealCategory = Joi.equal(...categories.map(({ id }) => id)).required()
|
||||||
|
|||||||
@@ -160,10 +160,12 @@ module.exports = function coalesceBadge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Use `coalesce()` to support empty labels and messages, as in the static
|
text: [
|
||||||
// badge.
|
// Use `coalesce()` to support empty labels and messages, as in the
|
||||||
label: coalesce(overrideLabel, serviceLabel, defaultLabel, category),
|
// static badge.
|
||||||
message: coalesce(serviceMessage, 'n/a'),
|
coalesce(overrideLabel, serviceLabel, defaultLabel, category),
|
||||||
|
coalesce(serviceMessage, 'n/a'),
|
||||||
|
],
|
||||||
color: coalesce(
|
color: coalesce(
|
||||||
// In case of an error, disregard user's color override.
|
// In case of an error, disregard user's color override.
|
||||||
isError ? undefined : overrideColor,
|
isError ? undefined : overrideColor,
|
||||||
@@ -177,7 +179,7 @@ module.exports = function coalesceBadge(
|
|||||||
serviceLabelColor,
|
serviceLabelColor,
|
||||||
defaultLabelColor
|
defaultLabelColor
|
||||||
),
|
),
|
||||||
style,
|
template: style,
|
||||||
namedLogo,
|
namedLogo,
|
||||||
logo: logoSvgBase64,
|
logo: logoSvgBase64,
|
||||||
logoWidth,
|
logoWidth,
|
||||||
|
|||||||
@@ -7,61 +7,63 @@ const coalesceBadge = require('./coalesce-badge')
|
|||||||
describe('coalesceBadge', function () {
|
describe('coalesceBadge', function () {
|
||||||
describe('Label', function () {
|
describe('Label', function () {
|
||||||
it('uses the default label', function () {
|
it('uses the default label', function () {
|
||||||
expect(coalesceBadge({}, {}, { label: 'heyo' })).to.include({
|
expect(coalesceBadge({}, {}, { label: 'heyo' }).text).to.deep.equal([
|
||||||
label: 'heyo',
|
'heyo',
|
||||||
})
|
'n/a',
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
// This behavior isn't great and we might want to remove it.
|
// This behavior isn't great and we might want to remove it.
|
||||||
it('uses the category as a default label', function () {
|
it('uses the category as a default label', function () {
|
||||||
expect(coalesceBadge({}, {}, {}, { category: 'cat' })).to.include({
|
expect(
|
||||||
label: 'cat',
|
coalesceBadge({}, {}, {}, { category: 'cat' }).text
|
||||||
})
|
).to.deep.equal(['cat', 'n/a'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preserves an empty label', function () {
|
it('preserves an empty label', function () {
|
||||||
expect(coalesceBadge({}, { label: '', message: '10k' }, {})).to.include({
|
expect(
|
||||||
label: '',
|
coalesceBadge({}, { label: '', message: '10k' }, {}).text
|
||||||
})
|
).to.deep.equal(['', '10k'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overrides the label', function () {
|
it('overrides the label', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {})
|
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {}).text
|
||||||
).to.include({ label: 'purr count' })
|
).to.deep.equal(['purr count', 'n/a'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Message', function () {
|
describe('Message', function () {
|
||||||
it('applies the service message', function () {
|
it('applies the service message', function () {
|
||||||
expect(coalesceBadge({}, { message: '10k' }, {})).to.include({
|
expect(coalesceBadge({}, { message: '10k' }, {}).text).to.deep.equal([
|
||||||
message: '10k',
|
undefined,
|
||||||
})
|
'10k',
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
// https://github.com/badges/shields/issues/1280
|
it('applies a numeric service message', function () {
|
||||||
it('converts a number to a string', function () {
|
|
||||||
// While a number of badges use this, in the long run we may want
|
// While a number of badges use this, in the long run we may want
|
||||||
// `render()` to always return a string.
|
// `render()` to always return a string.
|
||||||
expect(coalesceBadge({}, { message: 10 }, {})).to.include({
|
expect(coalesceBadge({}, { message: 10 }, {}).text).to.deep.equal([
|
||||||
message: 10,
|
undefined,
|
||||||
})
|
10,
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Right color', function () {
|
describe('Right color', function () {
|
||||||
it('uses the default color', function () {
|
it('uses the default color', function () {
|
||||||
expect(coalesceBadge({}, {}, {})).to.include({ color: 'lightgrey' })
|
expect(coalesceBadge({}, {}, {}).color).to.equal('lightgrey')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overrides the color', function () {
|
it('overrides the color', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {})
|
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {}).color
|
||||||
).to.include({ color: '10ADED' })
|
).to.equal('10ADED')
|
||||||
// also expected for legacy name
|
// also expected for legacy name
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {})
|
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {}).color
|
||||||
).to.include({ color: 'B0ADED' })
|
).to.equal('B0ADED')
|
||||||
})
|
})
|
||||||
|
|
||||||
context('In case of an error', function () {
|
context('In case of an error', function () {
|
||||||
@@ -71,23 +73,21 @@ describe('coalesceBadge', function () {
|
|||||||
{ color: '10ADED' },
|
{ color: '10ADED' },
|
||||||
{ isError: true, color: 'lightgray' },
|
{ isError: true, color: 'lightgray' },
|
||||||
{}
|
{}
|
||||||
)
|
).color
|
||||||
).to.include({ color: 'lightgray' })
|
).to.equal('lightgray')
|
||||||
// also expected for legacy name
|
// also expected for legacy name
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge(
|
coalesceBadge(
|
||||||
{ colorB: 'B0ADED' },
|
{ colorB: 'B0ADED' },
|
||||||
{ isError: true, color: 'lightgray' },
|
{ isError: true, color: 'lightgray' },
|
||||||
{}
|
{}
|
||||||
)
|
).color
|
||||||
).to.include({ color: 'lightgray' })
|
).to.equal('lightgray')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies the service color', function () {
|
it('applies the service color', function () {
|
||||||
expect(coalesceBadge({}, { color: 'red' }, {})).to.include({
|
expect(coalesceBadge({}, { color: 'red' }, {}).color).to.equal('red')
|
||||||
color: 'red',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,19 +97,20 @@ describe('coalesceBadge', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('applies the service label color', function () {
|
it('applies the service label color', function () {
|
||||||
expect(coalesceBadge({}, { labelColor: 'red' }, {})).to.include({
|
expect(coalesceBadge({}, { labelColor: 'red' }, {}).labelColor).to.equal(
|
||||||
labelColor: 'red',
|
'red'
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overrides the label color', function () {
|
it('overrides the label color', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {})
|
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {})
|
||||||
).to.include({ labelColor: '42f483' })
|
.labelColor
|
||||||
|
).to.equal('42f483')
|
||||||
// also expected for legacy name
|
// also expected for legacy name
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {})
|
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {}).labelColor
|
||||||
).to.include({ labelColor: 'B2f483' })
|
).to.equal('B2f483')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('converts a query-string numeric color to a string', function () {
|
it('converts a query-string numeric color to a string', function () {
|
||||||
@@ -119,8 +120,8 @@ describe('coalesceBadge', function () {
|
|||||||
{ color: 123 },
|
{ color: 123 },
|
||||||
{ color: 'green' },
|
{ color: 'green' },
|
||||||
{}
|
{}
|
||||||
)
|
).color
|
||||||
).to.include({ color: '123' })
|
).to.equal('123')
|
||||||
// also expected for legacy name
|
// also expected for legacy name
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge(
|
coalesceBadge(
|
||||||
@@ -128,8 +129,8 @@ describe('coalesceBadge', function () {
|
|||||||
{ colorB: 123 },
|
{ colorB: 123 },
|
||||||
{ color: 'green' },
|
{ color: 'green' },
|
||||||
{}
|
{}
|
||||||
)
|
).color
|
||||||
).to.include({ color: '123' })
|
).to.equal('123')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,9 +148,9 @@ describe('coalesceBadge', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('applies the named logo', function () {
|
it('applies the named logo', function () {
|
||||||
expect(coalesceBadge({}, { namedLogo: 'npm' }, {})).to.include({
|
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).namedLogo).to.equal(
|
||||||
namedLogo: 'npm',
|
'npm'
|
||||||
})
|
)
|
||||||
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).logo).to.equal(
|
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).logo).to.equal(
|
||||||
getShieldsIcon({ name: 'npm' })
|
getShieldsIcon({ name: 'npm' })
|
||||||
).and.not.to.be.empty
|
).and.not.to.be.empty
|
||||||
@@ -218,8 +219,8 @@ describe('coalesceBadge', function () {
|
|||||||
it('overrides the logo with custom svg', function () {
|
it('overrides the logo with custom svg', function () {
|
||||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {})
|
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {}).logo
|
||||||
).to.include({ logo: logoSvg })
|
).to.equal(logoSvg)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignores the color when custom svg is provided', function () {
|
it('ignores the color when custom svg is provided', function () {
|
||||||
@@ -229,36 +230,35 @@ describe('coalesceBadge', function () {
|
|||||||
{ logo: logoSvg, logoColor: 'brightgreen' },
|
{ logo: logoSvg, logoColor: 'brightgreen' },
|
||||||
{ namedLogo: 'appveyor' },
|
{ namedLogo: 'appveyor' },
|
||||||
{}
|
{}
|
||||||
)
|
).logo
|
||||||
).to.include({ logo: logoSvg })
|
).to.equal(logoSvg)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Logo width', function () {
|
describe('Logo width', function () {
|
||||||
it('overrides the logoWidth', function () {
|
it('overrides the logoWidth', function () {
|
||||||
expect(coalesceBadge({ logoWidth: 20 }, {}, {})).to.include({
|
expect(coalesceBadge({ logoWidth: 20 }, {}, {}).logoWidth).to.equal(20)
|
||||||
logoWidth: 20,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies the logo width', function () {
|
it('applies the logo width', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {})
|
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {}).logoWidth
|
||||||
).to.include({ logoWidth: 275 })
|
).to.equal(275)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Logo position', function () {
|
describe('Logo position', function () {
|
||||||
it('overrides the logoPosition', function () {
|
it('overrides the logoPosition', function () {
|
||||||
expect(coalesceBadge({ logoPosition: -10 }, {}, {})).to.include({
|
expect(
|
||||||
logoPosition: -10,
|
coalesceBadge({ logoPosition: -10 }, {}, {}).logoPosition
|
||||||
})
|
).to.equal(-10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies the logo position', function () {
|
it('applies the logo position', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {})
|
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {})
|
||||||
).to.include({ logoPosition: -10 })
|
.logoPosition
|
||||||
|
).to.equal(-10)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -279,24 +279,20 @@ describe('coalesceBadge', function () {
|
|||||||
|
|
||||||
describe('Style', function () {
|
describe('Style', function () {
|
||||||
it('falls back to flat with invalid style', function () {
|
it('falls back to flat with invalid style', function () {
|
||||||
expect(coalesceBadge({ style: 'pill' }, {}, {})).to.include({
|
expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('flat')
|
||||||
style: 'flat',
|
expect(coalesceBadge({ style: 7 }, {}, {}).template).to.equal('flat')
|
||||||
})
|
expect(coalesceBadge({ style: undefined }, {}, {}).template).to.equal(
|
||||||
expect(coalesceBadge({ style: 7 }, {}, {})).to.include({
|
'flat'
|
||||||
style: 'flat',
|
)
|
||||||
})
|
|
||||||
expect(coalesceBadge({ style: undefined }, {}, {})).to.include({
|
|
||||||
style: 'flat',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces legacy popout styles', function () {
|
it('replaces legacy popout styles', function () {
|
||||||
expect(coalesceBadge({ style: 'popout' }, {}, {})).to.include({
|
expect(coalesceBadge({ style: 'popout' }, {}, {}).template).to.equal(
|
||||||
style: 'flat',
|
'flat'
|
||||||
})
|
)
|
||||||
expect(coalesceBadge({ style: 'popout-square' }, {}, {})).to.include({
|
expect(
|
||||||
style: 'flat-square',
|
coalesceBadge({ style: 'popout-square' }, {}, {}).template
|
||||||
})
|
).to.equal('flat-square')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -304,7 +300,8 @@ describe('coalesceBadge', function () {
|
|||||||
it('overrides the cache length', function () {
|
it('overrides the cache length', function () {
|
||||||
expect(
|
expect(
|
||||||
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
|
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
|
||||||
).to.include({ cacheLengthSeconds: 123 })
|
.cacheLengthSeconds
|
||||||
|
).to.equal(123)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const camelcase = require('camelcase')
|
const camelcase = require('camelcase')
|
||||||
const BaseService = require('./base')
|
const BaseService = require('./base')
|
||||||
const { isValidCategory } = require('./categories')
|
const { isValidCategory } = require('./categories')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { pathToRegexp, compile } = require('path-to-regexp')
|
const { pathToRegexp, compile } = require('path-to-regexp')
|
||||||
const categories = require('../../services/categories')
|
const categories = require('../../services/categories')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
@@ -124,7 +124,12 @@ function transformExample(inExample, index, ServiceClass) {
|
|||||||
documentation,
|
documentation,
|
||||||
} = validateExample(inExample, index, ServiceClass)
|
} = validateExample(inExample, index, ServiceClass)
|
||||||
|
|
||||||
const { label, message, color, style, namedLogo } = coalesceBadge(
|
const {
|
||||||
|
text: [label, message],
|
||||||
|
color,
|
||||||
|
template: style,
|
||||||
|
namedLogo,
|
||||||
|
} = coalesceBadge(
|
||||||
{},
|
{},
|
||||||
staticPreview,
|
staticPreview,
|
||||||
ServiceClass.defaultBadgeData,
|
ServiceClass.defaultBadgeData,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
const BaseService = require('./base')
|
const BaseService = require('./base')
|
||||||
const BaseJsonService = require('./base-json')
|
const BaseJsonService = require('./base-json')
|
||||||
const BaseGraphqlService = require('./base-graphql')
|
const BaseGraphqlService = require('./base-graphql')
|
||||||
|
const NonMemoryCachingBaseService = require('./base-non-memory-caching')
|
||||||
const BaseStaticService = require('./base-static')
|
const BaseStaticService = require('./base-static')
|
||||||
const BaseSvgScrapingService = require('./base-svg-scraping')
|
const BaseSvgScrapingService = require('./base-svg-scraping')
|
||||||
const BaseXmlService = require('./base-xml')
|
const BaseXmlService = require('./base-xml')
|
||||||
@@ -21,6 +22,7 @@ module.exports = {
|
|||||||
BaseService,
|
BaseService,
|
||||||
BaseJsonService,
|
BaseJsonService,
|
||||||
BaseGraphqlService,
|
BaseGraphqlService,
|
||||||
|
NonMemoryCachingBaseService,
|
||||||
BaseStaticService,
|
BaseStaticService,
|
||||||
BaseSvgScrapingService,
|
BaseSvgScrapingService,
|
||||||
BaseXmlService,
|
BaseXmlService,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const request = require('request')
|
const request = require('request')
|
||||||
|
const queryString = require('query-string')
|
||||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||||
const { setCacheHeaders } = require('./cache-headers')
|
const { setCacheHeaders } = require('./cache-headers')
|
||||||
const {
|
const {
|
||||||
@@ -9,10 +10,27 @@ const {
|
|||||||
ShieldsRuntimeError,
|
ShieldsRuntimeError,
|
||||||
} = require('./errors')
|
} = require('./errors')
|
||||||
const { makeSend } = require('./legacy-result-sender')
|
const { makeSend } = require('./legacy-result-sender')
|
||||||
|
const LruCache = require('./lru-cache')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
|
|
||||||
const userAgent = 'Shields.io/2003a'
|
const userAgent = 'Shields.io/2003a'
|
||||||
|
|
||||||
|
// We avoid calling the vendor's server for computation of the information in a
|
||||||
|
// number of badges.
|
||||||
|
const minAccuracy = 0.75
|
||||||
|
|
||||||
|
// The quotient of (vendor) data change frequency by badge request frequency
|
||||||
|
// must be lower than this to trigger sending the cached data *before*
|
||||||
|
// updating our data from the vendor's server.
|
||||||
|
// Indeed, the accuracy of our badges are:
|
||||||
|
// A(Δt) = 1 - min(# data change over Δt, # requests over Δt)
|
||||||
|
// / (# requests over Δt)
|
||||||
|
// = 1 - max(1, df) / rf
|
||||||
|
const freqRatioMax = 1 - minAccuracy
|
||||||
|
|
||||||
|
// Request cache size of 5MB (~5000 bytes/image).
|
||||||
|
const requestCache = new LruCache(1000)
|
||||||
|
|
||||||
// These query parameters are available to any badge. They are handled by
|
// These query parameters are available to any badge. They are handled by
|
||||||
// `coalesceBadge`.
|
// `coalesceBadge`.
|
||||||
const globalQueryParams = new Set([
|
const globalQueryParams = new Set([
|
||||||
@@ -103,6 +121,8 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reqTime = new Date()
|
||||||
|
|
||||||
// `defaultCacheLengthSeconds` can be overridden by
|
// `defaultCacheLengthSeconds` can be overridden by
|
||||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||||
// by-badge basis). Then in turn that can be overridden by
|
// by-badge basis). Then in turn that can be overridden by
|
||||||
@@ -131,10 +151,49 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
filteredQueryParams[key] = queryParams[key]
|
filteredQueryParams[key] = queryParams[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Use sindresorhus query-string because it sorts the keys, whereas the
|
||||||
|
// builtin querystring module relies on the iteration order.
|
||||||
|
const stringified = queryString.stringify(filteredQueryParams)
|
||||||
|
const cacheIndex = `${match[0]}?${stringified}`
|
||||||
|
|
||||||
|
// Should we return the data right away?
|
||||||
|
const cached = requestCache.get(cacheIndex)
|
||||||
|
let cachedVersionSent = false
|
||||||
|
if (cached !== undefined) {
|
||||||
|
// A request was made not long ago.
|
||||||
|
const tooSoon = +reqTime - cached.time < cached.interval
|
||||||
|
if (tooSoon || cached.dataChange / cached.reqs <= freqRatioMax) {
|
||||||
|
const svg = makeBadge(cached.data.badgeData)
|
||||||
|
setCacheHeadersOnResponse(
|
||||||
|
ask.res,
|
||||||
|
cached.data.badgeData.cacheLengthSeconds
|
||||||
|
)
|
||||||
|
makeSend(cached.data.format, ask.res, end)(svg)
|
||||||
|
cachedVersionSent = true
|
||||||
|
// We do not wish to call the vendor servers.
|
||||||
|
if (tooSoon) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// In case our vendor servers are unresponsive.
|
// In case our vendor servers are unresponsive.
|
||||||
let serverUnresponsive = false
|
let serverUnresponsive = false
|
||||||
const serverResponsive = setTimeout(() => {
|
const serverResponsive = setTimeout(() => {
|
||||||
serverUnresponsive = true
|
serverUnresponsive = true
|
||||||
|
if (cachedVersionSent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requestCache.has(cacheIndex)) {
|
||||||
|
const cached = requestCache.get(cacheIndex)
|
||||||
|
const svg = makeBadge(cached.data.badgeData)
|
||||||
|
setCacheHeadersOnResponse(
|
||||||
|
ask.res,
|
||||||
|
cached.data.badgeData.cacheLengthSeconds
|
||||||
|
)
|
||||||
|
makeSend(cached.data.format, ask.res, end)(svg)
|
||||||
|
return
|
||||||
|
}
|
||||||
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||||
const badgeData = coalesceBadge(
|
const badgeData = coalesceBadge(
|
||||||
filteredQueryParams,
|
filteredQueryParams,
|
||||||
@@ -147,6 +206,8 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
makeSend(extension, ask.res, end)(svg)
|
makeSend(extension, ask.res, end)(svg)
|
||||||
}, 25000)
|
}, 25000)
|
||||||
|
|
||||||
|
// Only call vendor servers when last request is older than…
|
||||||
|
let cacheInterval = 5000 // milliseconds
|
||||||
function cachingRequest(uri, options, callback) {
|
function cachingRequest(uri, options, callback) {
|
||||||
if (typeof options === 'function' && !callback) {
|
if (typeof options === 'function' && !callback) {
|
||||||
callback = options
|
callback = options
|
||||||
@@ -162,7 +223,20 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
options.headers['User-Agent'] = userAgent
|
options.headers['User-Agent'] = userAgent
|
||||||
|
|
||||||
let bufferLength = 0
|
let bufferLength = 0
|
||||||
const r = request(options, callback)
|
const r = request(options, (err, res, body) => {
|
||||||
|
if (res != null && res.headers != null) {
|
||||||
|
const cacheControl = res.headers['cache-control']
|
||||||
|
if (cacheControl != null) {
|
||||||
|
const age = cacheControl.match(/max-age=([0-9]+)/)
|
||||||
|
// Would like to get some more test coverage on this before changing it.
|
||||||
|
// eslint-disable-next-line no-self-compare
|
||||||
|
if (age != null && +age[1] === +age[1]) {
|
||||||
|
cacheInterval = +age[1] * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(err, res, body)
|
||||||
|
})
|
||||||
r.on('data', chunk => {
|
r.on('data', chunk => {
|
||||||
bufferLength += chunk.length
|
bufferLength += chunk.length
|
||||||
if (bufferLength > fetchLimitBytes) {
|
if (bufferLength > fetchLimitBytes) {
|
||||||
@@ -190,11 +264,30 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearTimeout(serverResponsive)
|
clearTimeout(serverResponsive)
|
||||||
|
// Check for a change in the data.
|
||||||
|
let dataHasChanged = false
|
||||||
|
if (
|
||||||
|
cached !== undefined &&
|
||||||
|
cached.data.badgeData.text[1] !== badgeData.text[1]
|
||||||
|
) {
|
||||||
|
dataHasChanged = true
|
||||||
|
}
|
||||||
// Add format to badge data.
|
// Add format to badge data.
|
||||||
badgeData.format = format
|
badgeData.format = format
|
||||||
const svg = makeBadge(badgeData)
|
// Update information in the cache.
|
||||||
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
const updatedCache = {
|
||||||
makeSend(format, ask.res, end)(svg)
|
reqs: cached ? cached.reqs + 1 : 1,
|
||||||
|
dataChange: cached ? cached.dataChange + (dataHasChanged ? 1 : 0) : 1,
|
||||||
|
time: +reqTime,
|
||||||
|
interval: cacheInterval,
|
||||||
|
data: { format, badgeData },
|
||||||
|
}
|
||||||
|
requestCache.set(cacheIndex, updatedCache)
|
||||||
|
if (!cachedVersionSent) {
|
||||||
|
const svg = makeBadge(badgeData)
|
||||||
|
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
||||||
|
makeSend(format, ask.res, end)(svg)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cachingRequest
|
cachingRequest
|
||||||
)
|
)
|
||||||
@@ -206,8 +299,15 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearRequestCache() {
|
||||||
|
requestCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
handleRequest,
|
handleRequest,
|
||||||
promisify,
|
promisify,
|
||||||
|
clearRequestCache,
|
||||||
|
// Expose for testing.
|
||||||
|
_requestCache: requestCache,
|
||||||
userAgent,
|
userAgent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ const portfinder = require('portfinder')
|
|||||||
const Camp = require('@shields_io/camp')
|
const Camp = require('@shields_io/camp')
|
||||||
const got = require('../got-test-client')
|
const got = require('../got-test-client')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
const { handleRequest } = require('./legacy-request-handler')
|
const {
|
||||||
|
handleRequest,
|
||||||
|
clearRequestCache,
|
||||||
|
_requestCache,
|
||||||
|
} = require('./legacy-request-handler')
|
||||||
|
|
||||||
async function performTwoRequests(baseUrl, first, second) {
|
async function performTwoRequests(baseUrl, first, second) {
|
||||||
expect((await got(`${baseUrl}${first}`)).statusCode).to.equal(200)
|
expect((await got(`${baseUrl}${first}`)).statusCode).to.equal(200)
|
||||||
@@ -79,6 +83,7 @@ describe('The request handler', function () {
|
|||||||
camp.on('listening', () => done())
|
camp.on('listening', () => done())
|
||||||
})
|
})
|
||||||
afterEach(function (done) {
|
afterEach(function (done) {
|
||||||
|
clearRequestCache()
|
||||||
if (camp) {
|
if (camp) {
|
||||||
camp.close(() => done())
|
camp.close(() => done())
|
||||||
camp = null
|
camp = null
|
||||||
@@ -191,18 +196,57 @@ describe('The request handler', function () {
|
|||||||
|
|
||||||
describe('caching', function () {
|
describe('caching', function () {
|
||||||
describe('standard query parameters', function () {
|
describe('standard query parameters', function () {
|
||||||
|
let handlerCallCount
|
||||||
|
beforeEach(function () {
|
||||||
|
handlerCallCount = 0
|
||||||
|
})
|
||||||
|
|
||||||
function register({ cacheHeaderConfig }) {
|
function register({ cacheHeaderConfig }) {
|
||||||
camp.route(
|
camp.route(
|
||||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||||
handleRequest(
|
handleRequest(
|
||||||
cacheHeaderConfig,
|
cacheHeaderConfig,
|
||||||
(queryParams, match, sendBadge, request) => {
|
(queryParams, match, sendBadge, request) => {
|
||||||
|
++handlerCallCount
|
||||||
fakeHandler(queryParams, match, sendBadge, request)
|
fakeHandler(queryParams, match, sendBadge, request)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context('With standard cache settings', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
register({ cacheHeaderConfig: standardCacheHeaders })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should cache identical requests', async function () {
|
||||||
|
await performTwoRequests(
|
||||||
|
baseUrl,
|
||||||
|
'/testing/123.svg',
|
||||||
|
'/testing/123.svg'
|
||||||
|
)
|
||||||
|
expect(handlerCallCount).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should differentiate known query parameters', async function () {
|
||||||
|
await performTwoRequests(
|
||||||
|
baseUrl,
|
||||||
|
'/testing/123.svg?label=foo',
|
||||||
|
'/testing/123.svg?label=bar'
|
||||||
|
)
|
||||||
|
expect(handlerCallCount).to.equal(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore unknown query parameters', async function () {
|
||||||
|
await performTwoRequests(
|
||||||
|
baseUrl,
|
||||||
|
'/testing/123.svg?foo=1',
|
||||||
|
'/testing/123.svg?foo=2'
|
||||||
|
)
|
||||||
|
expect(handlerCallCount).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
|
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
|
||||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||||
@@ -233,6 +277,7 @@ describe('The request handler', function () {
|
|||||||
handleRequest(
|
handleRequest(
|
||||||
{ defaultCacheLengthSeconds: 300 },
|
{ defaultCacheLengthSeconds: 300 },
|
||||||
(queryParams, match, sendBadge, request) => {
|
(queryParams, match, sendBadge, request) => {
|
||||||
|
++handlerCallCount
|
||||||
createFakeHandlerWithCacheLength(400)(
|
createFakeHandlerWithCacheLength(400)(
|
||||||
queryParams,
|
queryParams,
|
||||||
match,
|
match,
|
||||||
@@ -253,6 +298,7 @@ describe('The request handler', function () {
|
|||||||
handleRequest(
|
handleRequest(
|
||||||
{ defaultCacheLengthSeconds: 300 },
|
{ defaultCacheLengthSeconds: 300 },
|
||||||
(queryParams, match, sendBadge, request) => {
|
(queryParams, match, sendBadge, request) => {
|
||||||
|
++handlerCallCount
|
||||||
createFakeHandlerWithCacheLength(200)(
|
createFakeHandlerWithCacheLength(200)(
|
||||||
queryParams,
|
queryParams,
|
||||||
match,
|
match,
|
||||||
@@ -299,6 +345,21 @@ describe('The request handler', function () {
|
|||||||
'no-cache, no-store, must-revalidate'
|
'no-cache, no-store, must-revalidate'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('the cache key', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
register({ cacheHeaderConfig: standardCacheHeaders })
|
||||||
|
})
|
||||||
|
const expectedCacheKey = '/testing/123.json?color=123&label=foo'
|
||||||
|
it('should match expected and use canonical order - 1', async function () {
|
||||||
|
await got(`${baseUrl}/testing/123.json?color=123&label=foo`)
|
||||||
|
expect(_requestCache.cache).to.have.keys(expectedCacheKey)
|
||||||
|
})
|
||||||
|
it('should match expected and use canonical order - 2', async function () {
|
||||||
|
await got(`${baseUrl}/testing/123.json?label=foo&color=123`)
|
||||||
|
expect(_requestCache.cache).to.have.keys(expectedCacheKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('custom query parameters', function () {
|
describe('custom query parameters', function () {
|
||||||
|
|||||||
136
core/base-service/lru-cache.js
Normal file
136
core/base-service/lru-cache.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
// In-memory KV, remove the oldest data when the capacity is reached.
|
||||||
|
|
||||||
|
const typeEnum = {
|
||||||
|
unit: 0,
|
||||||
|
heap: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// In bytes.
|
||||||
|
let heapSize
|
||||||
|
function computeHeapSize() {
|
||||||
|
return (heapSize = process.memoryUsage().heapTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
let heapSizeTimeout
|
||||||
|
function getHeapSize() {
|
||||||
|
if (heapSizeTimeout == null) {
|
||||||
|
// Compute the heap size every 60 seconds.
|
||||||
|
heapSizeTimeout = setInterval(computeHeapSize, 60 * 1000)
|
||||||
|
return computeHeapSize()
|
||||||
|
} else {
|
||||||
|
return heapSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CacheSlot(key, value) {
|
||||||
|
this.key = key
|
||||||
|
this.value = value
|
||||||
|
this.older = null // Newest slot that is older than this slot.
|
||||||
|
this.newer = null // Oldest slot that is newer than this slot.
|
||||||
|
}
|
||||||
|
|
||||||
|
function Cache(capacity, type) {
|
||||||
|
type = type || 'unit'
|
||||||
|
this.capacity = capacity
|
||||||
|
this.type = typeEnum[type]
|
||||||
|
this.cache = new Map() // Maps cache keys to CacheSlots.
|
||||||
|
this.newest = null // Newest slot in the cache.
|
||||||
|
this.oldest = null
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache.prototype = {
|
||||||
|
set: function addToCache(cacheKey, cached) {
|
||||||
|
let slot = this.cache.get(cacheKey)
|
||||||
|
if (slot === undefined) {
|
||||||
|
slot = new CacheSlot(cacheKey, cached)
|
||||||
|
this.cache.set(cacheKey, slot)
|
||||||
|
}
|
||||||
|
this.makeNewest(slot)
|
||||||
|
const numItemsToRemove = this.limitReached()
|
||||||
|
if (numItemsToRemove > 0) {
|
||||||
|
for (let i = 0; i < numItemsToRemove; i++) {
|
||||||
|
this.removeOldest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function getFromCache(cacheKey) {
|
||||||
|
const slot = this.cache.get(cacheKey)
|
||||||
|
if (slot !== undefined) {
|
||||||
|
this.makeNewest(slot)
|
||||||
|
return slot.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
has: function hasInCache(cacheKey) {
|
||||||
|
return this.cache.has(cacheKey)
|
||||||
|
},
|
||||||
|
|
||||||
|
makeNewest: function makeNewestSlot(slot) {
|
||||||
|
const previousNewest = this.newest
|
||||||
|
if (previousNewest === slot) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const older = slot.older
|
||||||
|
const newer = slot.newer
|
||||||
|
|
||||||
|
if (older !== null) {
|
||||||
|
older.newer = newer
|
||||||
|
} else if (newer !== null) {
|
||||||
|
this.oldest = newer
|
||||||
|
}
|
||||||
|
if (newer !== null) {
|
||||||
|
newer.older = older
|
||||||
|
}
|
||||||
|
this.newest = slot
|
||||||
|
|
||||||
|
if (previousNewest !== null) {
|
||||||
|
slot.older = previousNewest
|
||||||
|
slot.newer = null
|
||||||
|
previousNewest.newer = slot
|
||||||
|
} else {
|
||||||
|
// If previousNewest is null, the cache used to be empty.
|
||||||
|
this.oldest = slot
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeOldest: function removeOldest() {
|
||||||
|
const cacheKey = this.oldest.key
|
||||||
|
if (this.oldest !== null) {
|
||||||
|
this.oldest = this.oldest.newer
|
||||||
|
if (this.oldest !== null) {
|
||||||
|
this.oldest.older = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache.delete(cacheKey)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns the number of elements to remove if we're past the limit.
|
||||||
|
limitReached: function heuristic() {
|
||||||
|
if (this.type === typeEnum.unit) {
|
||||||
|
// Remove the excess.
|
||||||
|
return Math.max(0, this.cache.size - this.capacity)
|
||||||
|
} else if (this.type === typeEnum.heap) {
|
||||||
|
if (getHeapSize() >= this.capacity) {
|
||||||
|
console.log('LRU HEURISTIC heap:', getHeapSize())
|
||||||
|
// Remove half of them.
|
||||||
|
return this.cache.size >> 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown heuristic '${this.type}' for LRU cache.`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function () {
|
||||||
|
this.cache.clear()
|
||||||
|
this.newest = null
|
||||||
|
this.oldest = null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Cache
|
||||||
134
core/base-service/lru-cache.spec.js
Normal file
134
core/base-service/lru-cache.spec.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { expect } = require('chai')
|
||||||
|
const LRU = require('./lru-cache')
|
||||||
|
|
||||||
|
function expectCacheSlots(cache, keys) {
|
||||||
|
expect(cache.cache.size).to.equal(keys.length)
|
||||||
|
|
||||||
|
const slots = keys.map(k => cache.cache.get(k))
|
||||||
|
|
||||||
|
const first = slots[0]
|
||||||
|
const last = slots.slice(-1)[0]
|
||||||
|
|
||||||
|
expect(cache.oldest).to.equal(first)
|
||||||
|
expect(cache.newest).to.equal(last)
|
||||||
|
|
||||||
|
expect(first.older).to.be.null
|
||||||
|
expect(last.newer).to.be.null
|
||||||
|
|
||||||
|
for (let i = 0; i + 1 < slots.length; ++i) {
|
||||||
|
const current = slots[i]
|
||||||
|
const next = slots[i + 1]
|
||||||
|
expect(current.newer).to.equal(next)
|
||||||
|
expect(next.older).to.equal(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('The LRU cache', function () {
|
||||||
|
it('should support a zero capacity', function () {
|
||||||
|
const cache = new LRU(0)
|
||||||
|
cache.set('key', 'value')
|
||||||
|
expect(cache.cache.size).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support a one capacity', function () {
|
||||||
|
const cache = new LRU(1)
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
expectCacheSlots(cache, ['key1'])
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
expectCacheSlots(cache, ['key2'])
|
||||||
|
expect(cache.get('key1')).to.be.undefined
|
||||||
|
expect(cache.get('key2')).to.equal('value2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove the oldest element when reaching capacity', function () {
|
||||||
|
const cache = new LRU(2)
|
||||||
|
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
cache.set('key3', 'value3')
|
||||||
|
cache.cache.get('key1')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key2', 'key3'])
|
||||||
|
expect(cache.cache.get('key1')).to.be.undefined
|
||||||
|
expect(cache.get('key1')).to.be.undefined
|
||||||
|
expect(cache.get('key2')).to.equal('value2')
|
||||||
|
expect(cache.get('key3')).to.equal('value3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make sure that resetting a key in cache makes it newest', function () {
|
||||||
|
const cache = new LRU(2)
|
||||||
|
|
||||||
|
cache.set('key', 'value')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key', 'key2'])
|
||||||
|
|
||||||
|
cache.set('key', 'value')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key2', 'key'])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getting a key in the cache', function () {
|
||||||
|
context('when the requested key is oldest', function () {
|
||||||
|
it('should leave the keys in the expected order', function () {
|
||||||
|
const cache = new LRU(2)
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key1', 'key2'])
|
||||||
|
|
||||||
|
expect(cache.get('key1')).to.equal('value1')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key2', 'key1'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when the requested key is newest', function () {
|
||||||
|
it('should leave the keys in the expected order', function () {
|
||||||
|
const cache = new LRU(2)
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
|
||||||
|
expect(cache.get('key2')).to.equal('value2')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key1', 'key2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when the requested key is in the middle', function () {
|
||||||
|
it('should leave the keys in the expected order', function () {
|
||||||
|
const cache = new LRU(3)
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
cache.set('key3', 'value3')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key1', 'key2', 'key3'])
|
||||||
|
|
||||||
|
expect(cache.get('key2')).to.equal('value2')
|
||||||
|
|
||||||
|
expectCacheSlots(cache, ['key1', 'key3', 'key2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear', function () {
|
||||||
|
// Set up.
|
||||||
|
const cache = new LRU(2)
|
||||||
|
cache.set('key1', 'value1')
|
||||||
|
cache.set('key2', 'value2')
|
||||||
|
|
||||||
|
// Confidence check.
|
||||||
|
expect(cache.get('key1')).to.equal('value1')
|
||||||
|
expect(cache.get('key2')).to.equal('value2')
|
||||||
|
|
||||||
|
// Run.
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
// Test.
|
||||||
|
expect(cache.get('key1')).to.be.undefined
|
||||||
|
expect(cache.get('key2')).to.be.undefined
|
||||||
|
expect(cache.cache.size).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const camelcase = require('camelcase')
|
const camelcase = require('camelcase')
|
||||||
const emojic = require('emojic')
|
const emojic = require('emojic')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const queryString = require('query-string')
|
const queryString = require('query-string')
|
||||||
const BaseService = require('./base')
|
const BaseService = require('./base')
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const escapeStringRegexp = require('escape-string-regexp')
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { pathToRegexp } = require('path-to-regexp')
|
const { pathToRegexp } = require('path-to-regexp')
|
||||||
|
|
||||||
function makeFullUrl(base, partialUrl) {
|
function makeFullUrl(base, partialUrl) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { test, given, forCases } = require('sazerac')
|
const { test, given, forCases } = require('sazerac')
|
||||||
const {
|
const {
|
||||||
prepareRoute,
|
prepareRoute,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
|
|
||||||
// This should be kept in sync with the schema in
|
// This should be kept in sync with the schema in
|
||||||
// `frontend/lib/service-definitions/index.ts`.
|
// `frontend/lib/service-definitions/index.ts`.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const emojic = require('emojic')
|
const emojic = require('emojic')
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const trace = require('./trace')
|
const trace = require('./trace')
|
||||||
|
|
||||||
function validate(
|
function validate(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const trace = require('./trace')
|
const trace = require('./trace')
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ module.exports = class InfluxMetrics {
|
|||||||
const request = {
|
const request = {
|
||||||
uri: this._config.url,
|
uri: this._config.url,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: await this.metrics(),
|
body: this.metrics(),
|
||||||
timeout: this._config.timeoutMillseconds,
|
timeout: this._config.timeoutMillseconds,
|
||||||
auth,
|
auth,
|
||||||
}
|
}
|
||||||
@@ -51,8 +51,8 @@ module.exports = class InfluxMetrics {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async metrics() {
|
metrics() {
|
||||||
return promClientJsonToInfluxV2(await this._metricInstance.metrics(), {
|
return promClientJsonToInfluxV2(this._metricInstance.metrics(), {
|
||||||
env: this._config.envLabel,
|
env: this._config.envLabel,
|
||||||
application: 'shields',
|
application: 'shields',
|
||||||
instance: this._instanceId,
|
instance: this._instanceId,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe('Influx metrics', function () {
|
|||||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await influxMetrics.metrics()).to.contain('instance=instance3')
|
expect(influxMetrics.metrics()).to.contain('instance=instance3')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use a hostname as an instance label', async function () {
|
it('should use a hostname as an instance label', async function () {
|
||||||
@@ -46,9 +46,7 @@ describe('Influx metrics', function () {
|
|||||||
}
|
}
|
||||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||||
|
|
||||||
expect(await influxMetrics.metrics()).to.be.contain(
|
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
|
||||||
'instance=test-hostname'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use a random string as an instance label', async function () {
|
it('should use a random string as an instance label', async function () {
|
||||||
@@ -57,7 +55,7 @@ describe('Influx metrics', function () {
|
|||||||
}
|
}
|
||||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||||
|
|
||||||
expect(await influxMetrics.metrics()).to.be.match(/instance=\w+ /)
|
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use a hostname alias as an instance label', async function () {
|
it('should use a hostname alias as an instance label', async function () {
|
||||||
@@ -68,7 +66,7 @@ describe('Influx metrics', function () {
|
|||||||
}
|
}
|
||||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||||
|
|
||||||
expect(await influxMetrics.metrics()).to.be.contain(
|
expect(influxMetrics.metrics()).to.be.contain(
|
||||||
'instance=test-hostname-alias'
|
'instance=test-hostname-alias'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,26 +2,26 @@
|
|||||||
const groupBy = require('lodash.groupby')
|
const groupBy = require('lodash.groupby')
|
||||||
|
|
||||||
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
||||||
return metrics
|
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11
|
||||||
.flatMap(metric => {
|
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), [])
|
||||||
const valuesByLabels = groupBy(metric.values, value =>
|
return flatMap(metric => {
|
||||||
JSON.stringify(Object.entries(value.labels).sort())
|
const valuesByLabels = groupBy(metric.values, value =>
|
||||||
)
|
JSON.stringify(Object.entries(value.labels).sort())
|
||||||
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
)
|
||||||
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
||||||
.concat(Object.entries(extraLabels))
|
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.concat(Object.entries(extraLabels))
|
||||||
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.join(',')
|
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
|
||||||
const labelsFormatted = labels ? `,${labels}` : ''
|
.join(',')
|
||||||
const values = metricsWithSameLabel
|
const labelsFormatted = labels ? `,${labels}` : ''
|
||||||
.sort((a, b) => a.metricName.localeCompare(b.metricName))
|
const values = metricsWithSameLabel
|
||||||
.map(value => `${value.metricName || metric.name}=${value.value}`)
|
.sort((a, b) => a.metricName.localeCompare(b.metricName))
|
||||||
.join(',')
|
.map(value => `${value.metricName || metric.name}=${value.value}`)
|
||||||
return `prometheus${labelsFormatted} ${values}`
|
.join(',')
|
||||||
})
|
return `prometheus${labelsFormatted} ${values}`
|
||||||
}, metrics)
|
})
|
||||||
.join('\n')
|
}, metrics).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { promClientJsonToInfluxV2 }
|
module.exports = { promClientJsonToInfluxV2 }
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('Metric format converters', function () {
|
|||||||
expect(influx).to.be.equal('prometheus counter1=11')
|
expect(influx).to.be.equal('prometheus counter1=11')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('converts a counter (from prometheus registry)', async function () {
|
it('converts a counter (from prometheus registry)', function () {
|
||||||
const register = new prometheus.Registry()
|
const register = new prometheus.Registry()
|
||||||
const counter = new prometheus.Counter({
|
const counter = new prometheus.Counter({
|
||||||
name: 'counter1',
|
name: 'counter1',
|
||||||
@@ -31,7 +31,7 @@ describe('Metric format converters', function () {
|
|||||||
})
|
})
|
||||||
counter.inc(11)
|
counter.inc(11)
|
||||||
|
|
||||||
const influx = promClientJsonToInfluxV2(await register.getMetricsAsJSON())
|
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||||
|
|
||||||
expect(influx).to.be.equal('prometheus counter1=11')
|
expect(influx).to.be.equal('prometheus counter1=11')
|
||||||
})
|
})
|
||||||
@@ -52,7 +52,7 @@ describe('Metric format converters', function () {
|
|||||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('converts a gauge (from prometheus registry)', async function () {
|
it('converts a gauge (from prometheus registry)', function () {
|
||||||
const register = new prometheus.Registry()
|
const register = new prometheus.Registry()
|
||||||
const gauge = new prometheus.Gauge({
|
const gauge = new prometheus.Gauge({
|
||||||
name: 'gauge1',
|
name: 'gauge1',
|
||||||
@@ -61,7 +61,7 @@ describe('Metric format converters', function () {
|
|||||||
})
|
})
|
||||||
gauge.inc(20)
|
gauge.inc(20)
|
||||||
|
|
||||||
const influx = promClientJsonToInfluxV2(await register.getMetricsAsJSON())
|
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||||
|
|
||||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||||
})
|
})
|
||||||
@@ -101,7 +101,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('converts a histogram (from prometheus registry)', async function () {
|
it('converts a histogram (from prometheus registry)', function () {
|
||||||
const register = new prometheus.Registry()
|
const register = new prometheus.Registry()
|
||||||
const histogram = new prometheus.Histogram({
|
const histogram = new prometheus.Histogram({
|
||||||
name: 'histogram1',
|
name: 'histogram1',
|
||||||
@@ -113,7 +113,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
|
|||||||
histogram.observe(10)
|
histogram.observe(10)
|
||||||
histogram.observe(1)
|
histogram.observe(1)
|
||||||
|
|
||||||
const influx = promClientJsonToInfluxV2(await register.getMetricsAsJSON())
|
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||||
|
|
||||||
expect(sortLines(influx)).to.be.equal(
|
expect(sortLines(influx)).to.be.equal(
|
||||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||||
@@ -151,7 +151,7 @@ prometheus summary1_count=3,summary1_sum=111`)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('converts a summary (from prometheus registry)', async function () {
|
it('converts a summary (from prometheus registry)', function () {
|
||||||
const register = new prometheus.Registry()
|
const register = new prometheus.Registry()
|
||||||
const summary = new prometheus.Summary({
|
const summary = new prometheus.Summary({
|
||||||
name: 'summary1',
|
name: 'summary1',
|
||||||
@@ -163,7 +163,7 @@ prometheus summary1_count=3,summary1_sum=111`)
|
|||||||
summary.observe(10)
|
summary.observe(10)
|
||||||
summary.observe(1)
|
summary.observe(1)
|
||||||
|
|
||||||
const influx = promClientJsonToInfluxV2(await register.getMetricsAsJSON())
|
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||||
|
|
||||||
expect(sortLines(influx)).to.be.equal(
|
expect(sortLines(influx)).to.be.equal(
|
||||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const config = require('config').util.toObject()
|
||||||
|
const secretIsValid = require('./secret-is-valid')
|
||||||
const RateLimit = require('./rate-limit')
|
const RateLimit = require('./rate-limit')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
|
function secretInvalid(req, res) {
|
||||||
|
if (!secretIsValid(req.password)) {
|
||||||
|
// An unknown entity tries to connect. Let the connection linger for a minute.
|
||||||
|
setTimeout(() => {
|
||||||
|
res.json({ errors: [{ code: 'invalid_secrets' }] })
|
||||||
|
}, 10000)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function setRoutes({ rateLimit }, { server, metricInstance }) {
|
function setRoutes({ rateLimit }, { server, metricInstance }) {
|
||||||
const ipRateLimit = new RateLimit({
|
const ipRateLimit = new RateLimit({
|
||||||
@@ -15,6 +29,12 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
server.handle((req, res, next) => {
|
server.handle((req, res, next) => {
|
||||||
|
if (req.url.startsWith('/sys/')) {
|
||||||
|
if (secretInvalid(req, res)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (rateLimit) {
|
if (rateLimit) {
|
||||||
const ip =
|
const ip =
|
||||||
(req.headers['x-forwarded-for'] || '').split(', ')[0] ||
|
(req.headers['x-forwarded-for'] || '').split(', ')[0] ||
|
||||||
@@ -39,6 +59,27 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get('/sys/network', (req, res) => {
|
||||||
|
res.json({ ips: config.public.shields_ips })
|
||||||
|
})
|
||||||
|
|
||||||
|
server.ws('/sys/logs', socket => {
|
||||||
|
const listener = (...msg) => socket.send(msg.join(' '))
|
||||||
|
socket.on('close', () => log.removeListener(listener))
|
||||||
|
socket.on('message', msg => {
|
||||||
|
let req
|
||||||
|
try {
|
||||||
|
req = JSON.parse(msg)
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!secretIsValid(req.secret)) {
|
||||||
|
return socket.close()
|
||||||
|
}
|
||||||
|
log.addListener(listener)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
server.get('/sys/rate-limit', (req, res) => {
|
server.get('/sys/rate-limit', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
ip: ipRateLimit.toJSON(),
|
ip: ipRateLimit.toJSON(),
|
||||||
@@ -54,4 +95,6 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { setRoutes }
|
module.exports = {
|
||||||
|
setRoutes,
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ module.exports = class PrometheusMetrics {
|
|||||||
async registerMetricsEndpoint(server) {
|
async registerMetricsEndpoint(server) {
|
||||||
const { register } = this
|
const { register } = this
|
||||||
|
|
||||||
server.route(/^\/metrics$/, async (data, match, end, ask) => {
|
server.route(/^\/metrics$/, (data, match, end, ask) => {
|
||||||
ask.res.setHeader('Content-Type', register.contentType)
|
ask.res.setHeader('Content-Type', register.contentType)
|
||||||
ask.res.end(await register.metrics())
|
ask.res.end(register.metrics())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +90,8 @@ module.exports = class PrometheusMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async metrics() {
|
metrics() {
|
||||||
return await this.register.getMetricsAsJSON()
|
return this.register.getMetricsAsJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const serverSecrets = require('../../lib/server-secrets')
|
||||||
|
|
||||||
function constEq(a, b) {
|
function constEq(a, b) {
|
||||||
if (a.length !== b.length) {
|
if (a.length !== b.length) {
|
||||||
return false
|
return false
|
||||||
@@ -11,10 +13,9 @@ function constEq(a, b) {
|
|||||||
return zero === 0
|
return zero === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSecretIsValid(shieldsSecret) {
|
module.exports = function secretIsValid(secret = '') {
|
||||||
return function secretIsValid(secret = '') {
|
return (
|
||||||
return shieldsSecret && constEq(secret, shieldsSecret)
|
serverSecrets.shields_secret &&
|
||||||
}
|
constEq(secret, serverSecrets.shields_secret)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { makeSecretIsValid }
|
|
||||||
|
|||||||
@@ -6,16 +6,18 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const url = require('url')
|
const url = require('url')
|
||||||
const { URL } = url
|
const { URL } = url
|
||||||
const cloudflareMiddleware = require('cloudflare-middleware')
|
|
||||||
const bytes = require('bytes')
|
const bytes = require('bytes')
|
||||||
const Camp = require('@shields_io/camp')
|
const Camp = require('@shields_io/camp')
|
||||||
const originalJoi = require('joi')
|
const originalJoi = require('@hapi/joi')
|
||||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||||
const GithubConstellation = require('../../services/github/github-constellation')
|
const GithubConstellation = require('../../services/github/github-constellation')
|
||||||
const suggest = require('../../services/suggest')
|
const suggest = require('../../services/suggest')
|
||||||
const { loadServiceClasses } = require('../base-service/loader')
|
const { loadServiceClasses } = require('../base-service/loader')
|
||||||
const { makeSend } = require('../base-service/legacy-result-sender')
|
const { makeSend } = require('../base-service/legacy-result-sender')
|
||||||
const { handleRequest } = require('../base-service/legacy-request-handler')
|
const {
|
||||||
|
handleRequest,
|
||||||
|
clearRequestCache,
|
||||||
|
} = require('../base-service/legacy-request-handler')
|
||||||
const { clearRegularUpdateCache } = require('../legacy/regular-update')
|
const { clearRegularUpdateCache } = require('../legacy/regular-update')
|
||||||
const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
|
const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
|
||||||
const log = require('./log')
|
const log = require('./log')
|
||||||
@@ -87,10 +89,10 @@ const publicConfigSchema = Joi.object({
|
|||||||
.integer()
|
.integer()
|
||||||
.min(1)
|
.min(1)
|
||||||
.when('enabled', { is: true, then: Joi.required() }),
|
.when('enabled', { is: true, then: Joi.required() }),
|
||||||
intervalSeconds: Joi.number().integer().min(1).when('enabled', {
|
intervalSeconds: Joi.number()
|
||||||
is: true,
|
.integer()
|
||||||
then: Joi.required(),
|
.min(1)
|
||||||
}),
|
.when('enabled', { is: true, then: Joi.required() }),
|
||||||
instanceIdFrom: Joi.string()
|
instanceIdFrom: Joi.string()
|
||||||
.equal('hostname', 'env-var', 'random')
|
.equal('hostname', 'env-var', 'random')
|
||||||
.when('enabled', { is: true, then: Joi.required() }),
|
.when('enabled', { is: true, then: Joi.required() }),
|
||||||
@@ -115,6 +117,9 @@ const publicConfigSchema = Joi.object({
|
|||||||
cors: {
|
cors: {
|
||||||
allowedOrigin: Joi.array().items(optionalUrl).required(),
|
allowedOrigin: Joi.array().items(optionalUrl).required(),
|
||||||
},
|
},
|
||||||
|
persistence: {
|
||||||
|
dir: Joi.string().required(),
|
||||||
|
},
|
||||||
services: Joi.object({
|
services: Joi.object({
|
||||||
bitbucketServer: defaultService,
|
bitbucketServer: defaultService,
|
||||||
drone: defaultService,
|
drone: defaultService,
|
||||||
@@ -143,10 +148,6 @@ const publicConfigSchema = Joi.object({
|
|||||||
rateLimit: Joi.boolean().required(),
|
rateLimit: Joi.boolean().required(),
|
||||||
handleInternalErrors: Joi.boolean().required(),
|
handleInternalErrors: Joi.boolean().required(),
|
||||||
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
|
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
|
||||||
documentRoot: Joi.string().default(
|
|
||||||
path.resolve(__dirname, '..', '..', 'public')
|
|
||||||
),
|
|
||||||
requireCloudflare: Joi.boolean().required(),
|
|
||||||
}).required()
|
}).required()
|
||||||
|
|
||||||
const privateConfigSchema = Joi.object({
|
const privateConfigSchema = Joi.object({
|
||||||
@@ -167,6 +168,7 @@ const privateConfigSchema = Joi.object({
|
|||||||
npm_token: Joi.string(),
|
npm_token: Joi.string(),
|
||||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||||
sentry_dsn: Joi.string(),
|
sentry_dsn: Joi.string(),
|
||||||
|
shields_ips: Joi.array().items(Joi.string().ip()),
|
||||||
shields_secret: Joi.string(),
|
shields_secret: Joi.string(),
|
||||||
sl_insight_userUuid: Joi.string(),
|
sl_insight_userUuid: Joi.string(),
|
||||||
sl_insight_apiToken: Joi.string(),
|
sl_insight_apiToken: Joi.string(),
|
||||||
@@ -184,11 +186,6 @@ const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
|||||||
influx_username: Joi.string().required(),
|
influx_username: Joi.string().required(),
|
||||||
influx_password: Joi.string().required(),
|
influx_password: Joi.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
function addHandlerAtIndex(camp, index, handlerFn) {
|
|
||||||
camp.stack.splice(index, 0, handlerFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Server is based on the web framework Scoutcamp. It creates
|
* The Server is based on the web framework Scoutcamp. It creates
|
||||||
* an http server, sets up helpers for token persistence and monitoring.
|
* an http server, sets up helpers for token persistence and monitoring.
|
||||||
@@ -227,6 +224,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.githubConstellation = new GithubConstellation({
|
this.githubConstellation = new GithubConstellation({
|
||||||
|
persistence: publicConfig.persistence,
|
||||||
service: publicConfig.services.github,
|
service: publicConfig.services.github,
|
||||||
private: privateConfig,
|
private: privateConfig,
|
||||||
})
|
})
|
||||||
@@ -280,23 +278,6 @@ class Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
|
|
||||||
requireCloudflare() {
|
|
||||||
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
|
|
||||||
// by Express but not Scoutcamp.
|
|
||||||
addHandlerAtIndex(this.camp, 0, function (req, res, next) {
|
|
||||||
// On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
|
|
||||||
// the router ensures that the last item in the `X-Forwarded-For` header
|
|
||||||
// is the real origin.
|
|
||||||
// https://stackoverflow.com/a/18517550/893113
|
|
||||||
req.ip = process.env.DYNO
|
|
||||||
? req.headers['x-forwarded-for'].split(', ').pop()
|
|
||||||
: req.socket.remoteAddress
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up Scoutcamp routes for 404/not found responses
|
* Set up Scoutcamp routes for 404/not found responses
|
||||||
*/
|
*/
|
||||||
@@ -314,8 +295,7 @@ class Server {
|
|||||||
end
|
end
|
||||||
)(
|
)(
|
||||||
makeBadge({
|
makeBadge({
|
||||||
label: '410',
|
text: ['410', `${format} no longer available`],
|
||||||
message: `${format} no longer available`,
|
|
||||||
color: 'lightgray',
|
color: 'lightgray',
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
})
|
})
|
||||||
@@ -330,8 +310,7 @@ class Server {
|
|||||||
end
|
end
|
||||||
)(
|
)(
|
||||||
makeBadge({
|
makeBadge({
|
||||||
label: '404',
|
text: ['404', 'raster badges not available'],
|
||||||
message: 'raster badges not available',
|
|
||||||
color: 'lightgray',
|
color: 'lightgray',
|
||||||
format: 'svg',
|
format: 'svg',
|
||||||
})
|
})
|
||||||
@@ -349,8 +328,7 @@ class Server {
|
|||||||
end
|
end
|
||||||
)(
|
)(
|
||||||
makeBadge({
|
makeBadge({
|
||||||
label: '404',
|
text: ['404', 'badge not found'],
|
||||||
message: 'badge not found',
|
|
||||||
color: 'red',
|
color: 'red',
|
||||||
format,
|
format,
|
||||||
})
|
})
|
||||||
@@ -431,25 +409,19 @@ class Server {
|
|||||||
ssl: { isSecure: secure, cert, key },
|
ssl: { isSecure: secure, cert, key },
|
||||||
cors: { allowedOrigin },
|
cors: { allowedOrigin },
|
||||||
rateLimit,
|
rateLimit,
|
||||||
requireCloudflare,
|
|
||||||
} = this.config.public
|
} = this.config.public
|
||||||
|
|
||||||
log(`Server is starting up: ${this.baseUrl}`)
|
log(`Server is starting up: ${this.baseUrl}`)
|
||||||
|
|
||||||
const camp = (this.camp = Camp.create({
|
const camp = (this.camp = Camp.create({
|
||||||
documentRoot: this.config.public.documentRoot,
|
documentRoot: path.resolve(__dirname, '..', '..', 'public'),
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
secure,
|
secure,
|
||||||
staticMaxAge: 300,
|
|
||||||
cert,
|
cert,
|
||||||
key,
|
key,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (requireCloudflare) {
|
|
||||||
this.requireCloudflare()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { metricInstance } = this
|
const { metricInstance } = this
|
||||||
this.cleanupMonitor = sysMonitor.setRoutes(
|
this.cleanupMonitor = sysMonitor.setRoutes(
|
||||||
{ rateLimit },
|
{ rateLimit },
|
||||||
@@ -482,6 +454,7 @@ class Server {
|
|||||||
static resetGlobalState() {
|
static resetGlobalState() {
|
||||||
// This state should be migrated to instance state. When possible, do not add new
|
// This state should be migrated to instance state. When possible, do not add new
|
||||||
// global state.
|
// global state.
|
||||||
|
clearRequestCache()
|
||||||
clearRegularUpdateCache()
|
clearRegularUpdateCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const isSvg = require('is-svg')
|
const isSvg = require('is-svg')
|
||||||
const config = require('config')
|
const config = require('config')
|
||||||
const nock = require('nock')
|
|
||||||
const sinon = require('sinon')
|
|
||||||
const got = require('../got-test-client')
|
const got = require('../got-test-client')
|
||||||
const Server = require('./server')
|
const Server = require('./server')
|
||||||
const { createTestServer } = require('./in-process-server-test-helpers')
|
const { createTestServer } = require('./in-process-server-test-helpers')
|
||||||
@@ -16,11 +13,7 @@ describe('The server', function () {
|
|||||||
before('Start the server', async function () {
|
before('Start the server', async function () {
|
||||||
// Fixes https://github.com/badges/shields/issues/2611
|
// Fixes https://github.com/badges/shields/issues/2611
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
server = await createTestServer({
|
server = await createTestServer()
|
||||||
public: {
|
|
||||||
documentRoot: path.resolve(__dirname, 'test-public'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
baseUrl = server.baseUrl
|
baseUrl = server.baseUrl
|
||||||
await server.start()
|
await server.start()
|
||||||
})
|
})
|
||||||
@@ -52,16 +45,6 @@ describe('The server', function () {
|
|||||||
.and.to.include('apple')
|
.and.to.include('apple')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should serve front-end with default maxAge', async function () {
|
|
||||||
const { headers } = await got(`${baseUrl}/`)
|
|
||||||
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should serve badges with custom maxAge', async function () {
|
|
||||||
const { headers } = await got(`${baseUrl}npm/l/express`)
|
|
||||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should redirect colorscheme PNG badges as configured', async function () {
|
it('should redirect colorscheme PNG badges as configured', async function () {
|
||||||
const { statusCode, headers } = await got(
|
const { statusCode, headers } = await got(
|
||||||
`${baseUrl}:fruit-apple-green.png`,
|
`${baseUrl}:fruit-apple-green.png`,
|
||||||
@@ -185,28 +168,6 @@ describe('The server', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
context('`requireCloudflare` is enabled', function () {
|
|
||||||
let server
|
|
||||||
afterEach(async function () {
|
|
||||||
if (server) {
|
|
||||||
server.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reject requests from localhost with an empty 200 response', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
server = await createTestServer({ public: { requireCloudflare: true } })
|
|
||||||
await server.start()
|
|
||||||
|
|
||||||
const { statusCode, body } = await got(
|
|
||||||
`${server.baseUrl}badge/foo-bar-blue.svg`
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(statusCode).to.be.equal(200)
|
|
||||||
expect(body).to.equal('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('configuration', function () {
|
describe('configuration', function () {
|
||||||
let server
|
let server
|
||||||
afterEach(async function () {
|
afterEach(async function () {
|
||||||
@@ -368,67 +329,4 @@ describe('The server', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('running with metrics enabled', function () {
|
|
||||||
let server, baseUrl, scope, clock
|
|
||||||
const metricsPushIntervalSeconds = 1
|
|
||||||
before('Start the server', async function () {
|
|
||||||
// Fixes https://github.com/badges/shields/issues/2611
|
|
||||||
this.timeout(10000)
|
|
||||||
process.env.INSTANCE_ID = 'test-instance'
|
|
||||||
server = await createTestServer({
|
|
||||||
public: {
|
|
||||||
metrics: {
|
|
||||||
prometheus: { enabled: true },
|
|
||||||
influx: {
|
|
||||||
enabled: true,
|
|
||||||
url: 'http://localhost:1112/metrics',
|
|
||||||
instanceIdFrom: 'env-var',
|
|
||||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
|
||||||
envLabel: 'localhost-env',
|
|
||||||
intervalSeconds: metricsPushIntervalSeconds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
private: {
|
|
||||||
influx_username: 'influx-username',
|
|
||||||
influx_password: 'influx-password',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
clock = sinon.useFakeTimers()
|
|
||||||
baseUrl = server.baseUrl
|
|
||||||
await server.start()
|
|
||||||
})
|
|
||||||
after('Shut down the server', async function () {
|
|
||||||
if (server) {
|
|
||||||
await server.stop()
|
|
||||||
}
|
|
||||||
server = undefined
|
|
||||||
nock.cleanAll()
|
|
||||||
delete process.env.INSTANCE_ID
|
|
||||||
clock.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should push custom metrics', async function () {
|
|
||||||
scope = nock('http://localhost:1112', {
|
|
||||||
reqheaders: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.post(
|
|
||||||
'/metrics',
|
|
||||||
/prometheus,application=shields,category=static,env=localhost-env,family=static-badge,instance=test-instance,service=static_badge service_requests_total=1\n/
|
|
||||||
)
|
|
||||||
.basicAuth({ user: 'influx-username', pass: 'influx-password' })
|
|
||||||
.reply(200)
|
|
||||||
await got(`${baseUrl}badge/fruit-apple-green.svg`)
|
|
||||||
|
|
||||||
await clock.tickAsync(1000 * metricsPushIntervalSeconds + 500)
|
|
||||||
|
|
||||||
expect(scope.isDone()).to.be.equal(
|
|
||||||
true,
|
|
||||||
`pending mocks: ${scope.pendingMocks()}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>shields.io</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
concise, consistent, legible
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
51
core/token-pooling/fs-token-persistence.js
Normal file
51
core/token-pooling/fs-token-persistence.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const fsos = require('fsos')
|
||||||
|
const TokenPersistence = require('./token-persistence')
|
||||||
|
|
||||||
|
class FsTokenPersistence extends TokenPersistence {
|
||||||
|
constructor({ path }) {
|
||||||
|
super()
|
||||||
|
this.path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
let contents
|
||||||
|
try {
|
||||||
|
contents = await fsos.get(this.path)
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
contents = '[]'
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = JSON.parse(contents)
|
||||||
|
this._tokens = new Set(tokens)
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const tokens = Array.from(this._tokens)
|
||||||
|
await fsos.set(this.path, JSON.stringify(tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTokenAdded(token) {
|
||||||
|
if (!this._tokens) {
|
||||||
|
throw Error('initialize() has not been called')
|
||||||
|
}
|
||||||
|
this._tokens.add(token)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTokenRemoved(token) {
|
||||||
|
if (!this._tokens) {
|
||||||
|
throw Error('initialize() has not been called')
|
||||||
|
}
|
||||||
|
this._tokens.delete(token)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FsTokenPersistence
|
||||||
72
core/token-pooling/fs-token-persistence.spec.js
Normal file
72
core/token-pooling/fs-token-persistence.spec.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const tmp = require('tmp')
|
||||||
|
const readFile = require('fs-readfile-promise')
|
||||||
|
const { expect } = require('chai')
|
||||||
|
const FsTokenPersistence = require('./fs-token-persistence')
|
||||||
|
|
||||||
|
describe('File system token persistence', function () {
|
||||||
|
let path, persistence
|
||||||
|
beforeEach(function () {
|
||||||
|
path = tmp.tmpNameSync()
|
||||||
|
persistence = new FsTokenPersistence({ path })
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when the file does not exist', function () {
|
||||||
|
it('does nothing', async function () {
|
||||||
|
const tokens = await persistence.initialize()
|
||||||
|
expect(tokens).to.deep.equal([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saving creates an empty file', async function () {
|
||||||
|
await persistence.initialize()
|
||||||
|
|
||||||
|
await persistence.save()
|
||||||
|
|
||||||
|
const json = JSON.parse(await readFile(path))
|
||||||
|
expect(json).to.deep.deep.equal([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when the file exists', function () {
|
||||||
|
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
fs.writeFileSync(path, JSON.stringify(initialTokens))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the contents', async function () {
|
||||||
|
const tokens = await persistence.initialize()
|
||||||
|
expect(tokens).to.deep.equal(initialTokens)
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when tokens are added', function () {
|
||||||
|
it('saves the change', async function () {
|
||||||
|
const newToken = 'e'.repeat(40)
|
||||||
|
const expected = Array.from(initialTokens)
|
||||||
|
expected.push(newToken)
|
||||||
|
|
||||||
|
await persistence.initialize()
|
||||||
|
await persistence.noteTokenAdded(newToken)
|
||||||
|
|
||||||
|
const savedTokens = JSON.parse(await readFile(path))
|
||||||
|
expect(savedTokens).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('when tokens are removed', function () {
|
||||||
|
it('saves the change', async function () {
|
||||||
|
const expected = Array.from(initialTokens)
|
||||||
|
const toRemove = expected.pop()
|
||||||
|
|
||||||
|
await persistence.initialize()
|
||||||
|
|
||||||
|
await persistence.noteTokenRemoved(toRemove)
|
||||||
|
|
||||||
|
const savedTokens = JSON.parse(await readFile(path))
|
||||||
|
expect(savedTokens).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
const { URL } = require('url')
|
const { URL } = require('url')
|
||||||
const Redis = require('ioredis')
|
const Redis = require('ioredis')
|
||||||
const log = require('../server/log')
|
const log = require('../server/log')
|
||||||
|
const TokenPersistence = require('./token-persistence')
|
||||||
|
|
||||||
module.exports = class RedisTokenPersistence {
|
module.exports = class RedisTokenPersistence extends TokenPersistence {
|
||||||
constructor({ url, key }) {
|
constructor({ url, key }) {
|
||||||
|
super()
|
||||||
this.url = url
|
this.url = url
|
||||||
this.key = key
|
this.key = key
|
||||||
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
|
||||||
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
@@ -40,20 +40,4 @@ module.exports = class RedisTokenPersistence {
|
|||||||
async onTokenRemoved(token) {
|
async onTokenRemoved(token) {
|
||||||
await this.redis.srem(this.key, token)
|
await this.redis.srem(this.key, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
async noteTokenAdded(token) {
|
|
||||||
try {
|
|
||||||
await this.onTokenAdded(token)
|
|
||||||
} catch (e) {
|
|
||||||
log.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async noteTokenRemoved(token) {
|
|
||||||
try {
|
|
||||||
await this.onTokenRemoved(token)
|
|
||||||
} catch (e) {
|
|
||||||
log.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
core/token-pooling/token-persistence.js
Normal file
44
core/token-pooling/token-persistence.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const log = require('../server/log')
|
||||||
|
|
||||||
|
// This is currently bound to the legacy github auth code. That will be
|
||||||
|
// replaced with a dependency-injected token provider.
|
||||||
|
class TokenPersistence {
|
||||||
|
constructor() {
|
||||||
|
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
||||||
|
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
throw Error('initialize() is not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {}
|
||||||
|
|
||||||
|
async onTokenAdded(token) {
|
||||||
|
throw Error('onTokenAdded() is not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
async noteTokenAdded(token) {
|
||||||
|
try {
|
||||||
|
await this.onTokenAdded(token)
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTokenRemoved(token) {
|
||||||
|
throw Error('onTokenRemoved() is not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
async noteTokenRemoved(token) {
|
||||||
|
try {
|
||||||
|
await this.onTokenRemoved(token)
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TokenPersistence
|
||||||
@@ -111,7 +111,10 @@ if (allFiles.length > 100) {
|
|||||||
|
|
||||||
// eslint-disable-next-line promise/prefer-await-to-then
|
// eslint-disable-next-line promise/prefer-await-to-then
|
||||||
danger.git.diffForFile(file).then(({ diff }) => {
|
danger.git.diffForFile(file).then(({ diff }) => {
|
||||||
if (diff.includes('authHelper') && !secretsDocs.modified) {
|
if (
|
||||||
|
(diff.includes('authHelper') || diff.includes('serverSecrets')) &&
|
||||||
|
!secretsDocs.modified
|
||||||
|
) {
|
||||||
warn(
|
warn(
|
||||||
[
|
[
|
||||||
`:books: Remember to ensure any changes to \`config.private\` `,
|
`:books: Remember to ensure any changes to \`config.private\` `,
|
||||||
@@ -131,11 +134,11 @@ if (allFiles.length > 100) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff.includes("require('@hapi/joi')")) {
|
if (diff.includes("require('joi')")) {
|
||||||
fail(
|
fail(
|
||||||
[
|
[
|
||||||
`Found import of '@hapi/joi' in \`${file}\`. <br>`,
|
`Found import of 'joi' in \`${file}\`. <br>`,
|
||||||
"Joi must be imported as 'joi'.",
|
"Joi must be imported as '@hapi/joi'.",
|
||||||
].join('')
|
].join('')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ const { renderVersionBadge } = require('..//version')
|
|||||||
const { BaseJsonService } = require('..')
|
const { BaseJsonService } = require('..')
|
||||||
|
|
||||||
// (4)
|
// (4)
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
version: Joi.string().required(),
|
version: Joi.string().required(),
|
||||||
}).required()
|
}).required()
|
||||||
@@ -226,7 +226,7 @@ Description of the code:
|
|||||||
- [text-formatters.js](https://github.com/badges/shields/blob/master/services/text-formatters.js)
|
- [text-formatters.js](https://github.com/badges/shields/blob/master/services/text-formatters.js)
|
||||||
- [version.js](https://github.com/badges/shields/blob/master/services/version.js)
|
- [version.js](https://github.com/badges/shields/blob/master/services/version.js)
|
||||||
3. Our badge will query a JSON API so we will extend `BaseJsonService` instead of `BaseService`. This contains some helpers to reduce the need for boilerplate when calling a JSON API.
|
3. Our badge will query a JSON API so we will extend `BaseJsonService` instead of `BaseService`. This contains some helpers to reduce the need for boilerplate when calling a JSON API.
|
||||||
4. We perform input validation by defining a schema which we expect the JSON we receive to conform to. This is done using [Joi](https://github.com/hapijs/joi). Defining a schema means we can ensure the JSON we receive meets our expectations and throw an error if we receive unexpected input without having to explicitly code validation checks. The schema also acts as a filter on the JSON object. Any properties we're going to reference need to be validated, otherwise they will be filtered out. In this case our schema declares that we expect to receive an object which must have a property called 'version', which is a string. There is further documentation on [input validation](input-validation.md).
|
4. We perform input validation by defining a schema which we expect the JSON we receive to conform to. This is done using [Joi](https://github.com/hapijs/joi). Defining a schema means we can ensure the JSON we receive meets our expectations and throw an error if we receive unexpected input without having to explicitly code validation checks. The schema also acts as a filter on the JSON object. Any properties we're going to reference need to be validated, otherwise they will be filtered out. In this case our schema declares that we expect to receive an object which must have a property called 'version', which is a string.
|
||||||
5. Our module exports a class which extends `BaseJsonService`
|
5. Our module exports a class which extends `BaseJsonService`
|
||||||
6. Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main [shields.io](https://shields.io) website. [Here](https://github.com/badges/shields/blob/master/services/categories.js) is the list of the valid categories. See [section 4.4](#44-adding-an-example-to-the-front-page) for more details on examples.
|
6. Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main [shields.io](https://shields.io) website. [Here](https://github.com/badges/shields/blob/master/services/categories.js) is the list of the valid categories. See [section 4.4](#44-adding-an-example-to-the-front-page) for more details on examples.
|
||||||
7. As with our previous badge, we need to declare a route. This time we will capture a variable called `gem`.
|
7. As with our previous badge, we need to declare a route. This time we will capture a variable called `gem`.
|
||||||
@@ -311,7 +311,7 @@ Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).
|
|||||||
|
|
||||||
If you update `examples`, you don't have to restart the server. Run `npm run defs` in another terminal window and the frontend will update.
|
If you update `examples`, you don't have to restart the server. Run `npm run defs` in another terminal window and the frontend will update.
|
||||||
|
|
||||||
### (4.5) Write Tests<!-- Change the link below when you change the heading -->
|
### (4.5) Write Tests <!-- Change the link below when you change the heading -->
|
||||||
|
|
||||||
[write tests]: #45-write-tests
|
[write tests]: #45-write-tests
|
||||||
|
|
||||||
|
|||||||
@@ -125,9 +125,10 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
|||||||
registered.)
|
registered.)
|
||||||
2. Scoutcamp invokes a callback with the four parameters:
|
2. Scoutcamp invokes a callback with the four parameters:
|
||||||
`( queryParams, match, end, ask )`. This callback is defined in
|
`( queryParams, match, end, ask )`. This callback is defined in
|
||||||
[`legacy-request-handler`][legacy-request-handler]. A timeout is set to
|
[`legacy-request-handler`][legacy-request-handler]. If the badge result
|
||||||
handle unresponsive service code and the next callback is invoked: the
|
is found in a relatively small in-memory cache, the response is sent
|
||||||
legacy handler function.
|
immediately. Otherwise a timeout is set to handle unresponsive service
|
||||||
|
code and the next callback is invoked: the legacy handler function.
|
||||||
3. The legacy handler function receives
|
3. The legacy handler function receives
|
||||||
`( queryParams, match, sendBadge, request )`. Its job is to extract data
|
`( queryParams, match, sendBadge, request )`. Its job is to extract data
|
||||||
from the regex `match` and `queryParams`, invoke `request` to fetch
|
from the regex `match` and `queryParams`, invoke `request` to fetch
|
||||||
@@ -161,8 +162,8 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
|||||||
service’s defaults to produce an object that fully describes the badge to
|
service’s defaults to produce an object that fully describes the badge to
|
||||||
be rendered.
|
be rendered.
|
||||||
9. `sendBadge` is invoked with that object. It does some housekeeping on the
|
9. `sendBadge` is invoked with that object. It does some housekeeping on the
|
||||||
timeout. Then it renders the badge to svg or raster and pushes out the
|
timeout and caches the result. Then it renders the badge to svg or raster
|
||||||
result over the HTTPS connection.
|
and pushes out the result over the HTTPS connection.
|
||||||
|
|
||||||
[error reporting]: https://github.com/badges/shields/blob/master/doc/production-hosting.md#error-reporting
|
[error reporting]: https://github.com/badges/shields/blob/master/doc/production-hosting.md#error-reporting
|
||||||
[coalescebadge]: https://github.com/badges/shields/blob/master/core/base-service/coalesce-badge.js
|
[coalescebadge]: https://github.com/badges/shields/blob/master/core/base-service/coalesce-badge.js
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
# Input Data Validation
|
|
||||||
|
|
||||||
When we receive input data from an upstream API, we perform input validation to:
|
|
||||||
|
|
||||||
- Ensure we won't throw a runtime error trying to render a badge
|
|
||||||
- Ensure we won't render badges with spurious or unexpected output e.g:     etc
|
|
||||||
- Express and document our understanding of the input data
|
|
||||||
|
|
||||||
## Writing schemas and validation
|
|
||||||
|
|
||||||
- The default validation mechanism should be to use [Joi](https://github.com/sideway/joi) to define a schema for the input data. Validation against Joi schemas is implemented in the base classes and inherited by every service class that extends them. Sometimes additional manual validation is needed which can't be covered by Joi and plugins in which case we implement it by hand.
|
|
||||||
|
|
||||||
- If validation is implemented manually (because we need to enforce a constraint that can't be expressed with Joi), invalid data should throw an [InvalidResponse](https://contributing.shields.io/module-core_base-service_errors-InvalidResponse.html) exception.
|
|
||||||
|
|
||||||
- Our definition of "valid" should not be stricter than the upstream API's definition of "valid".
|
|
||||||
|
|
||||||
- The schema/validation we choose is informed by the assumptions we're making about the data. e.g:
|
|
||||||
|
|
||||||
- If we're going to use a value, make sure it exists.
|
|
||||||
- If we need to multiply it by something, we check it's a number.
|
|
||||||
- If we're going to call `.split()` on it, we make sure it's a string.
|
|
||||||
- If we're going to address `foo[0]`, `foo` must be an array.
|
|
||||||
- If we're going to sort a version on the assumption it is a semver, check it's a semver
|
|
||||||
|
|
||||||
- We don't need to validate characteristics we don't rely on. For example, if we're just going to render a version on a badge with the same exact value from the API response and do not need to sort or transform the value, then it doesn't matter what format the version number is in. We can use a very relaxed schema to validate in this case, e.g. `Joi.string().required()`
|
|
||||||
|
|
||||||
- If theory (docs) and practice (real-world API responses) conflict, real-world outputs take precedence over documented behaviour. e.g: if the docs say version is a semver but we learn that there are real-world packages where the version number is `0.3b` or `1.2.1.27` then we should accept those values in preference to enforcing the documented API behaviour.
|
|
||||||
|
|
||||||
- Shields is descriptive rather than prescriptive. We reflect the established norms of the communities we serve.
|
|
||||||
|
|
||||||
- It is fine to define a single schema which is applied to multiple badges. For example, we could define a schema that says:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const schema = Joi.object({
|
|
||||||
license: Joi.string().required(),
|
|
||||||
version: Joi.string().required(),
|
|
||||||
}).required()
|
|
||||||
```
|
|
||||||
|
|
||||||
and have both the license and version badges validate the response against that schema.
|
|
||||||
|
|
||||||
- For build status badges there is a shared [isBuildStatus](https://github.com/badges/shields/blob/master/services/build-status.js) validator. In most cases build status badges should use `isBuildStatus` or input validation and `renderBuildStatusBadge` should be used for rendering. Any additional status values can be added to the relevant color arrays.
|
|
||||||
|
|
||||||
## Identifying problems
|
|
||||||
|
|
||||||
- If we know of a real-world example of a package/repo/etc that causes us to render an invalid value on a badge (e.g:    ) our input validation is broken and we should fix it.
|
|
||||||
|
|
||||||
- If we know of a real-world example of a package/repo/etc that causes us to throw an unhandled runtime exception, our input validation is broken and we should fix it.
|
|
||||||
|
|
||||||
- We should not fail to render a badge because of a validation failure on a field that isn't necessary to render the badge. In the above example of a shared license/version schema: If we become aware of a real-world example of a package/repo/etc that has a `version` key but not a `license` key then we should split the schema (or make `version` optional and handle the error in code).
|
|
||||||
@@ -14,43 +14,55 @@ Production hosting is managed by the Shields ops team:
|
|||||||
[operations issues]: https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3Aoperations
|
[operations issues]: https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3Aoperations
|
||||||
[ops discord]: https://discordapp.com/channels/308323056592486420/480747695879749633
|
[ops discord]: https://discordapp.com/channels/308323056592486420/480747695879749633
|
||||||
|
|
||||||
| Component | Subcomponent | People with access |
|
| Component | Subcomponent | People with access |
|
||||||
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
|
| ----------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| shields-production-us | Account owner | @paulmelnikow |
|
| shields-production-us | Account owner | @paulmelnikow |
|
||||||
| shields-production-us | Full access | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
| shields-production-us | Full access | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||||
| shields-production-us | Access management | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
| shields-production-us | Access management | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||||
| Compose.io Redis | Account owner | @paulmelnikow |
|
| Compose.io Redis | Account owner | @paulmelnikow |
|
||||||
| Compose.io Redis | Account access | @paulmelnikow |
|
| Compose.io Redis | Account access | @paulmelnikow |
|
||||||
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||||
| Zeit Now | Team owner | @paulmelnikow |
|
| Zeit Now | Team owner | @paulmelnikow |
|
||||||
| Zeit Now | Team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
| Zeit Now | Team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||||
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||||
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||||
| Cloudflare (CDN) | Account owner | @espadrine |
|
| Legacy badge servers | Account owner | @espadrine |
|
||||||
| Cloudflare (CDN) | Access management | @espadrine |
|
| Legacy badge servers | ssh, logs | @espadrine |
|
||||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
| Legacy badge servers | Deployment | @espadrine, @paulmelnikow |
|
||||||
| Twitch | OAuth app | @PyvesB |
|
| Legacy badge servers | Admin endpoints | @espadrine, @paulmelnikow |
|
||||||
| Discord | OAuth app | @PyvesB |
|
| Cloudflare (CDN) | Account owner | @espadrine |
|
||||||
| YouTube | Account owner | @PyvesB |
|
| Cloudflare (CDN) | Access management | @espadrine |
|
||||||
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
|
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||||
| DNS | Account owner | @olivierlacan |
|
| Twitch | OAuth app | @PyvesB |
|
||||||
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
| Discord | OAuth app | @PyvesB |
|
||||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
| YouTube | Account owner | @PyvesB |
|
||||||
| Metrics server | Owner | @platan |
|
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
|
||||||
| UptimeRobot | Account owner | @paulmelnikow |
|
| DNS | Account owner | @olivierlacan |
|
||||||
| More metrics | Owner | @RedSparr0w |
|
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
||||||
|
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||||
|
| Frontend | Deployment | Technically anyone with push access but in practice must be deployed with the badge server |
|
||||||
|
| Metrics server | Owner | @platan |
|
||||||
|
| UptimeRobot | Account owner | @paulmelnikow |
|
||||||
|
| More metrics | Owner | @RedSparr0w |
|
||||||
|
| Netlify (documentation site) | Owner | @chris48s |
|
||||||
|
|
||||||
|
There are [too many bottlenecks][issue 2577]!
|
||||||
|
|
||||||
|
[issue 2577]: https://github.com/badges/shields/issues/2577
|
||||||
|
|
||||||
## Attached state
|
## Attached state
|
||||||
|
|
||||||
Shields has mercifully little persistent state:
|
Shields has mercifully little persistent state:
|
||||||
|
|
||||||
1. The GitHub tokens we collect are saved on each server in a cloud Redis
|
1. The GitHub tokens we collect are saved on each server in a cloud Redis database.
|
||||||
database. They can also be fetched from the [GitHub auth admin endpoint][]
|
They can also be fetched from the [GitHub auth admin endpoint][] for debugging.
|
||||||
for debugging.
|
2. The server keeps a few caches in memory. These are neither persisted nor
|
||||||
2. The server keeps the [regular-update cache][] in memory. It is neither
|
inspectable.
|
||||||
persisted nor inspectable.
|
- The [request cache][]
|
||||||
|
- The [regular-update cache][]
|
||||||
|
|
||||||
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
|
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
|
||||||
|
[request cache]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js#L29-L30
|
||||||
[regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js
|
[regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -78,17 +90,32 @@ files:
|
|||||||
[shields-io-production.yml]: ../config/shields-io-production.yml
|
[shields-io-production.yml]: ../config/shields-io-production.yml
|
||||||
[default.yml]: ../config/default.yml
|
[default.yml]: ../config/default.yml
|
||||||
|
|
||||||
|
The project ships with `dotenv`, however there is no `.env` in production.
|
||||||
|
|
||||||
## Badge CDN
|
## Badge CDN
|
||||||
|
|
||||||
Sitting in front of the three servers is a Cloudflare Free account which
|
Sitting in front of the three servers is a Cloudflare Free account which
|
||||||
provides several services:
|
provides several services:
|
||||||
|
|
||||||
- Global CDN, caching, and SSL gateway for `img.shields.io` and `shields.io`
|
- Global CDN, caching, and SSL gateway for `img.shields.io`
|
||||||
- Analytics through the Cloudflare dashboard
|
- Analytics through the Cloudflare dashboard
|
||||||
- DNS resolution for `shields.io` (and subdomains)
|
- DNS hosting for `shields.io`
|
||||||
|
|
||||||
Cloudflare is configured to respect the servers' cache headers.
|
Cloudflare is configured to respect the servers' cache headers.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
The frontend is served by [GitHub Pages][] via the [gh-pages branch][gh-pages]. SSL is enforced.
|
||||||
|
|
||||||
|
`shields.io` resolves to the GitHub Pages hosts. It is not proxied through
|
||||||
|
Cloudflare.
|
||||||
|
|
||||||
|
Technically any maintainer can push to `gh-pages`, but in practice the frontend must be deployed
|
||||||
|
with the badge server via the deployment process described below.
|
||||||
|
|
||||||
|
[github pages]: https://pages.github.com/
|
||||||
|
[gh-pages]: https://github.com/badges/shields/tree/gh-pages
|
||||||
|
|
||||||
## Raster server
|
## Raster server
|
||||||
|
|
||||||
The raster server `raster.shields.io` (a.k.a. the rasterizing proxy) is
|
The raster server `raster.shields.io` (a.k.a. the rasterizing proxy) is
|
||||||
@@ -98,14 +125,28 @@ hosted on [Zeit Now][]. It's managed in the
|
|||||||
[zeit now]: https://zeit.co/now
|
[zeit now]: https://zeit.co/now
|
||||||
[svg-to-image-proxy]: https://github.com/badges/svg-to-image-proxy
|
[svg-to-image-proxy]: https://github.com/badges/svg-to-image-proxy
|
||||||
|
|
||||||
### Heroku Deployment
|
## Deployment
|
||||||
|
|
||||||
Both the badge server and frontend are served from Heroku.
|
The deployment is done in two stages: the badge server (heroku) and the front-end (gh-pages).
|
||||||
|
|
||||||
|
### Heroku
|
||||||
|
|
||||||
After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the `shields-staging` pipeline and review http://shields-staging.herokuapp.com/
|
After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the `shields-staging` pipeline and review http://shields-staging.herokuapp.com/
|
||||||
|
|
||||||
If we're happy with it, "promote to production". This will deploy what's on staging to the `shields-production-eu` and `shields-production-us` pieplines.
|
If we're happy with it, "promote to production". This will deploy what's on staging to the `shields-production-eu` and `shields-production-us` pieplines.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
To deploy the front-end to GH pages, use a clean clone of the shields repo.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ git pull # update the working copy
|
||||||
|
$ npm ci # install dependencies (devDependencies are needed to build the frontend)
|
||||||
|
$ make deploy-gh-pages # build the frontend and push it to the gh-pages branch
|
||||||
|
```
|
||||||
|
|
||||||
|
No secrets are required to build or deploy the frontend.
|
||||||
|
|
||||||
## DNS
|
## DNS
|
||||||
|
|
||||||
DNS is registered with [DNSimple][].
|
DNS is registered with [DNSimple][].
|
||||||
@@ -128,13 +169,6 @@ the server. It's generously donated by [Sentry][sentry home]. We bundle
|
|||||||
[sentry home]: https://sentry.io/shields/
|
[sentry home]: https://sentry.io/shields/
|
||||||
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
|
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
|
||||||
|
|
||||||
## URLs
|
|
||||||
|
|
||||||
The canonical and only recommended domain for badge URLs is `img.shields.io`. Currently it is possible to request badges on both `img.shields.io` and `shields.io` i.e: https://img.shields.io/badge/build-passing-brightgreen and https://shields.io/badge/build-passing-brightgreen will both work. However:
|
|
||||||
|
|
||||||
- We never show or generate the `img.`-less URL format on https://shields.io/
|
|
||||||
- We make no guarantees about the `img.`-less URL format. At some future point we may remove the ability to serve badges on `shields.io` (without `img.`) without any warning. `img.shields.io` should always be used for badge urls.
|
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Overall server performance and requests by service are monitored using
|
Overall server performance and requests by service are monitored using
|
||||||
@@ -154,3 +188,19 @@ Request performance is monitored in two places:
|
|||||||
[monitor]: https://shields.redsparr0w.com/1568/
|
[monitor]: https://shields.redsparr0w.com/1568/
|
||||||
[notifications]: http://shields.redsparr0w.com/discord_notification
|
[notifications]: http://shields.redsparr0w.com/discord_notification
|
||||||
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646
|
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646
|
||||||
|
|
||||||
|
## Legacy servers
|
||||||
|
|
||||||
|
There are three legacy servers on OVH VPS’s which are currently used for proxying.
|
||||||
|
|
||||||
|
| Cname | Hostname | Type | IP | Location |
|
||||||
|
| --------------------------- | -------------------- | ---- | -------------- | ------------------ |
|
||||||
|
| [s0.servers.shields.io][s0] | vps71670.vps.ovh.ca | VPS | 192.99.59.72 | Quebec, Canada |
|
||||||
|
| [s1.servers.shields.io][s1] | vps244529.ovh.net | VPS | 51.254.114.150 | Gravelines, France |
|
||||||
|
| [s2.servers.shields.io][s2] | vps117870.vps.ovh.ca | VPS | 149.56.96.133 | Quebec, Canada |
|
||||||
|
|
||||||
|
[s0]: https://s0.servers.shields.io/index.html
|
||||||
|
[s1]: https://s1.servers.shields.io/index.html
|
||||||
|
[s2]: https://s2.servers.shields.io/index.html
|
||||||
|
|
||||||
|
The only way to inspect the commit on the server is with `git ls-remote`.
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ Once the route is working, fill out `render()` and `handle()`.
|
|||||||
<details>
|
<details>
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { errorMessagesFor } = require('./github-helpers')
|
const { errorMessagesFor } = require('./github-helpers')
|
||||||
|
|
||||||
const issueSchema = Joi.object({
|
const issueSchema = Joi.object({
|
||||||
@@ -174,7 +174,7 @@ or create an abstract superclass like **PypiBase**:
|
|||||||
<details>
|
<details>
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const BaseJsonService = require('../base-json')
|
const BaseJsonService = require('../base-json')
|
||||||
|
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
|
|||||||
@@ -184,30 +184,19 @@ Start the server using the Sentry DSN. You can set it:
|
|||||||
sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
|
sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
|
||||||
```
|
```
|
||||||
|
|
||||||
Or via config as you would do with [server secrets](server-secrets.md):
|
- or by `sentry_dsn` secret property defined in `private/secret.json`
|
||||||
|
|
||||||
```yml
|
|
||||||
private:
|
|
||||||
sentry_dsn: ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo node server
|
sudo node server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Prometheus
|
### Prometheus
|
||||||
|
|
||||||
Shields uses [prom-client](https://github.com/siimon/prom-client) to provide [default metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors). These metrics are disabled by default.
|
Shields uses [prom-client](https://github.com/siimon/prom-client) to provide [default metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors). These metrics are disabled by default.
|
||||||
You can enable them by `METRICS_PROMETHEUS_ENABLED` and `METRICS_PROMETHEUS_ENDPOINT_ENABLED` environment variables.
|
You can enable them by `METRICS_PROMETHEUS_ENABLED` environment variable.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
METRICS_PROMETHEUS_ENABLED=true METRICS_PROMETHEUS_ENDPOINT_ENABLED=true npm start
|
METRICS_PROMETHEUS_ENABLED=true npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Metrics are available at `/metrics` resource.
|
Metrics are available at `/metrics` resource.
|
||||||
|
|
||||||
### Cloudflare
|
|
||||||
|
|
||||||
Shields uses Cloudflare as a downstream CDN. If your installation does the same,
|
|
||||||
you can configure your server to only accept requests coming from Cloudflare's IPs.
|
|
||||||
Set `public.requireCloudflare: true`.
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ t.create('Build status')
|
|||||||
- Note that when we call our badge, we are allowing it to communicate with an external service without mocking the response. We write tests which interact with external services, which is unusual practice in unit testing. We do this because one of the purposes of service tests is to notify us if a badge has broken due to an upstream API change. For this reason it is important for at least one test to call the live API without mocking the interaction.
|
- Note that when we call our badge, we are allowing it to communicate with an external service without mocking the response. We write tests which interact with external services, which is unusual practice in unit testing. We do this because one of the purposes of service tests is to notify us if a badge has broken due to an upstream API change. For this reason it is important for at least one test to call the live API without mocking the interaction.
|
||||||
- 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  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.
|
- 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  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.
|
- 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.
|
3. `expectBadge()` is a helper function which accepts either a string literal or a [Joi][] schema for the different fields.
|
||||||
Joi is a validation library that is build 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
|
match based on a set of allowed strings, regexes, or specific values. You can
|
||||||
refer to their [API reference][joi api].
|
refer to their [API reference][joi api].
|
||||||
@@ -82,7 +82,6 @@ harness will call it for you.
|
|||||||
[icedfrisby api]: https://github.com/MarkHerhold/IcedFrisby/blob/master/API.md
|
[icedfrisby api]: https://github.com/MarkHerhold/IcedFrisby/blob/master/API.md
|
||||||
[joi]: https://github.com/hapijs/joi
|
[joi]: https://github.com/hapijs/joi
|
||||||
[joi api]: https://github.com/hapijs/joi/blob/master/API.md
|
[joi api]: https://github.com/hapijs/joi/blob/master/API.md
|
||||||
[regexp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
|
|
||||||
|
|
||||||
### (3) Running the Tests
|
### (3) Running the Tests
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
CopiedContentIndicatorHandle,
|
CopiedContentIndicatorHandle,
|
||||||
} from './copied-content-indicator'
|
} from './copied-content-indicator'
|
||||||
|
|
||||||
|
function getBaseUrlFromWindowLocation(): string {
|
||||||
|
// Default to the current hostname for when there is no `BASE_URL` set
|
||||||
|
// at build time (as in most PaaS deploys).
|
||||||
|
const { protocol, hostname } = window.location
|
||||||
|
return `${protocol}//${hostname}`
|
||||||
|
}
|
||||||
|
|
||||||
export default function Customizer({
|
export default function Customizer({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
title,
|
title,
|
||||||
@@ -32,7 +39,9 @@ export default function Customizer({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
||||||
const indicatorRef = useRef<CopiedContentIndicatorHandle>() as React.MutableRefObject<CopiedContentIndicatorHandle>
|
const indicatorRef = useRef<
|
||||||
|
CopiedContentIndicatorHandle
|
||||||
|
>() as React.MutableRefObject<CopiedContentIndicatorHandle>
|
||||||
const [path, setPath] = useState('')
|
const [path, setPath] = useState('')
|
||||||
const [queryString, setQueryString] = useState<string>()
|
const [queryString, setQueryString] = useState<string>()
|
||||||
const [pathIsComplete, setPathIsComplete] = useState<boolean>()
|
const [pathIsComplete, setPathIsComplete] = useState<boolean>()
|
||||||
@@ -41,7 +50,7 @@ export default function Customizer({
|
|||||||
|
|
||||||
function generateBuiltBadgeUrl(): string {
|
function generateBuiltBadgeUrl(): string {
|
||||||
const suffix = queryString ? `?${queryString}` : ''
|
const suffix = queryString ? `?${queryString}` : ''
|
||||||
return `${baseUrl}${path}${suffix}`
|
return `${baseUrl || getBaseUrlFromWindowLocation()}${path}${suffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLivePreview(): JSX.Element {
|
function renderLivePreview(): JSX.Element {
|
||||||
|
|||||||
@@ -91,15 +91,15 @@ export function constructPath({
|
|||||||
if (typeof token === 'string') {
|
if (typeof token === 'string') {
|
||||||
return token.trim()
|
return token.trim()
|
||||||
} else {
|
} else {
|
||||||
const { prefix, name, modifier } = token
|
const { delimiter, name, optional } = token
|
||||||
const value = namedParams[name]
|
const value = namedParams[name]
|
||||||
if (value) {
|
if (value) {
|
||||||
return `${prefix}${value.trim()}`
|
return `${delimiter}${value.trim()}`
|
||||||
} else if (modifier === '?' || modifier === '*') {
|
} else if (optional) {
|
||||||
return ''
|
return ''
|
||||||
} else {
|
} else {
|
||||||
isComplete = false
|
isComplete = false
|
||||||
return `${prefix}:${name}`
|
return `${delimiter}:${name}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -221,15 +221,14 @@ export default function PathBuilder({
|
|||||||
tokenIndex: number,
|
tokenIndex: number,
|
||||||
namedParamIndex: number
|
namedParamIndex: number
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const { prefix, modifier } = token
|
const { delimiter, optional } = token
|
||||||
const optional = modifier === '?' || modifier === '*'
|
|
||||||
const name = `${token.name}`
|
const name = `${token.name}`
|
||||||
|
|
||||||
const exampleValue = exampleParams[name] || '(not set)'
|
const exampleValue = exampleParams[name] || '(not set)'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={token.name}>
|
<React.Fragment key={token.name}>
|
||||||
{renderLiteral(prefix, tokenIndex, false)}
|
{renderLiteral(delimiter, tokenIndex, false)}
|
||||||
<PathBuilderColumn pathContainsOnlyLiterals={false} withHorizPadding>
|
<PathBuilderColumn pathContainsOnlyLiterals={false} withHorizPadding>
|
||||||
<NamedParamLabelContainer>
|
<NamedParamLabelContainer>
|
||||||
<BuilderLabel htmlFor={name}>{humanizeString(name)}</BuilderLabel>
|
<BuilderLabel htmlFor={name}>{humanizeString(name)}</BuilderLabel>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||||
import { getBaseUrl } from '../../constants'
|
import { baseUrl } from '../../constants'
|
||||||
import { shieldsLogos, simpleIcons } from '../../lib/supported-features'
|
import { shieldsLogos, simpleIcons } from '../../lib/supported-features'
|
||||||
import Meta from '../meta'
|
import Meta from '../meta'
|
||||||
import Header from '../header'
|
import Header from '../header'
|
||||||
@@ -19,7 +19,6 @@ const StyledTable = styled.table`
|
|||||||
`
|
`
|
||||||
|
|
||||||
function NamedLogoTable({ logoNames }: { logoNames: string[] }): JSX.Element {
|
function NamedLogoTable({ logoNames }: { logoNames: string[] }): JSX.Element {
|
||||||
const baseUrl = getBaseUrl()
|
|
||||||
return (
|
return (
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { Fragment } from 'react'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
// @ts-ingnore
|
// @ts-ingnore
|
||||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||||
import { getBaseUrl } from '../../constants'
|
import { baseUrl } from '../../constants'
|
||||||
import Meta from '../meta'
|
import Meta from '../meta'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Header from '../header'
|
import Header from '../header'
|
||||||
@@ -123,14 +123,13 @@ const examples = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function StyleTable({ style }: { style: string }): JSX.Element {
|
function StyleTable({ style }: { style: string }): JSX.Element {
|
||||||
const baseUrl = getBaseUrl()
|
|
||||||
return (
|
return (
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>Badges (new)</td>
|
<td>Badges (new)</td>
|
||||||
<td>Badges (img.shields.io)</td>
|
<td>Badges (old)</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
RenderableExample,
|
RenderableExample,
|
||||||
} from '../lib/service-definitions'
|
} from '../lib/service-definitions'
|
||||||
import ServiceDefinitionSetHelper from '../lib/service-definitions/service-definition-set-helper'
|
import ServiceDefinitionSetHelper from '../lib/service-definitions/service-definition-set-helper'
|
||||||
import { getBaseUrl } from '../constants'
|
import { baseUrl } from '../constants'
|
||||||
import Meta from './meta'
|
import Meta from './meta'
|
||||||
import Header from './header'
|
import Header from './header'
|
||||||
import SuggestionAndSearch from './suggestion-and-search'
|
import SuggestionAndSearch from './suggestion-and-search'
|
||||||
@@ -54,7 +54,6 @@ export default function Main({
|
|||||||
setSelectedExampleIsSuggestion,
|
setSelectedExampleIsSuggestion,
|
||||||
] = useState(false)
|
] = useState(false)
|
||||||
const searchTimeout = useRef(0)
|
const searchTimeout = useRef(0)
|
||||||
const baseUrl = getBaseUrl()
|
|
||||||
|
|
||||||
function performSearch(query: string): void {
|
function performSearch(query: string): void {
|
||||||
setSearchIsInProgress(false)
|
setSearchIsInProgress(false)
|
||||||
|
|||||||
@@ -1,33 +1 @@
|
|||||||
const baseUrl = process.env.GATSBY_BASE_URL
|
export const baseUrl = process.env.GATSBY_BASE_URL || ''
|
||||||
|
|
||||||
export function getBaseUrl(): string {
|
|
||||||
if (baseUrl) {
|
|
||||||
return baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is a special case for production.
|
|
||||||
|
|
||||||
We want to be able to build the front end with no value set for
|
|
||||||
`GATSBY_BASE_URL` so that we can deploy a build to staging
|
|
||||||
and then promote the exact same build to production.
|
|
||||||
|
|
||||||
When deployed to staging, we want the frontend on
|
|
||||||
https://staging.shields.io/ to generate badges with the base
|
|
||||||
https://staging.shields.io/
|
|
||||||
|
|
||||||
When we promote to production we want https://shields.io/ and
|
|
||||||
https://www.shields.io/ to both generate badges with the base
|
|
||||||
https://img.shields.io/
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
const { protocol, hostname } = window.location
|
|
||||||
if (['shields.io', 'www.shields.io'].includes(hostname)) {
|
|
||||||
return 'https://img.shields.io'
|
|
||||||
}
|
|
||||||
return `${protocol}//${hostname}`
|
|
||||||
} catch (e) {
|
|
||||||
// server-side rendering
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
8
frontend/enzyme-conf.spec.js
Normal file
8
frontend/enzyme-conf.spec.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Enzyme from 'enzyme'
|
||||||
|
import Adapter from 'enzyme-adapter-react-16'
|
||||||
|
import chai from 'chai'
|
||||||
|
import chaiEnzyme from 'chai-enzyme'
|
||||||
|
|
||||||
|
Enzyme.configure({ adapter: new Adapter() })
|
||||||
|
|
||||||
|
chai.use(chaiEnzyme())
|
||||||
@@ -21,11 +21,19 @@ export function removeRegexpFromPattern(pattern: string): string {
|
|||||||
if (typeof token === 'string') {
|
if (typeof token === 'string') {
|
||||||
return token
|
return token
|
||||||
} else {
|
} else {
|
||||||
const { prefix, modifier, name, pattern } = token
|
const { delimiter, optional, repeat, name, pattern } = token
|
||||||
if (typeof name === 'number') {
|
if (typeof name === 'number') {
|
||||||
return `${prefix}(${pattern})`
|
return `${delimiter}(${pattern})`
|
||||||
} else {
|
} else {
|
||||||
return `${prefix}:${name}${modifier}`
|
let modifier = ''
|
||||||
|
if (optional && !repeat) {
|
||||||
|
modifier = '?'
|
||||||
|
} else if (!optional && repeat) {
|
||||||
|
modifier = '+'
|
||||||
|
} else if (optional && repeat) {
|
||||||
|
modifier = '*'
|
||||||
|
}
|
||||||
|
return `${delimiter}:${name}${modifier}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { getBaseUrl } from '../constants'
|
import { baseUrl } from '../constants'
|
||||||
import Meta from '../components/meta'
|
import Meta from '../components/meta'
|
||||||
import Header from '../components/header'
|
import Header from '../components/header'
|
||||||
import Footer from '../components/footer'
|
import Footer from '../components/footer'
|
||||||
@@ -19,7 +19,6 @@ const SponsorContainer = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default function SponsorsPage(): JSX.Element {
|
export default function SponsorsPage(): JSX.Element {
|
||||||
const baseUrl = getBaseUrl()
|
|
||||||
return (
|
return (
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
@@ -115,6 +114,9 @@ export default function SponsorsPage(): JSX.Element {
|
|||||||
<li>
|
<li>
|
||||||
<a href="https://lgtm.com/">LGTM</a>
|
<a href="https://lgtm.com/">LGTM</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.netlify.com/">Netlify</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
||||||
import { getBaseUrl } from '../constants'
|
import { baseUrl } from '../constants'
|
||||||
import Meta from '../components/meta'
|
import Meta from '../components/meta'
|
||||||
import Header from '../components/header'
|
import Header from '../components/header'
|
||||||
import Footer from '../components/footer'
|
import Footer from '../components/footer'
|
||||||
@@ -89,7 +89,6 @@ const Schema = styled.dl`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default function EndpointPage(): JSX.Element {
|
export default function EndpointPage(): JSX.Element {
|
||||||
const baseUrl = getBaseUrl()
|
|
||||||
return (
|
return (
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
@@ -210,7 +209,7 @@ export default function EndpointPage(): JSX.Element {
|
|||||||
<dt>logoColor</dt>
|
<dt>logoColor</dt>
|
||||||
<dd>
|
<dd>
|
||||||
Default: none. Same meaning as the query string. Can be overridden by
|
Default: none. Same meaning as the query string. Can be overridden by
|
||||||
the query string. Only works for named logos.
|
the query string.
|
||||||
</dd>
|
</dd>
|
||||||
<dt>logoWidth</dt>
|
<dt>logoWidth</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
siteMetadata: {
|
siteMetadata: {
|
||||||
title: 'Shields.io: Quality metadata badges for open source projects',
|
title: 'Shields.io: Quality metadata badges for open source projects',
|
||||||
@@ -13,7 +11,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
resolve: 'gatsby-plugin-page-creator',
|
resolve: 'gatsby-plugin-page-creator',
|
||||||
options: {
|
options: {
|
||||||
path: path.join(__dirname, 'frontend', 'pages'),
|
path: `${__dirname}/frontend/pages`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'gatsby-plugin-react-helmet',
|
'gatsby-plugin-react-helmet',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const envFlag = require('node-env-flag')
|
|||||||
|
|
||||||
const includeDevPages = envFlag(process.env.INCLUDE_DEV_PAGES, true)
|
const includeDevPages = envFlag(process.env.INCLUDE_DEV_PAGES, true)
|
||||||
|
|
||||||
const { categories } = yaml.load(
|
const { categories } = yaml.safeLoad(
|
||||||
fs.readFileSync('./service-definitions.yml', 'utf8')
|
fs.readFileSync('./service-definitions.yml', 'utf8')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const {
|
const {
|
||||||
toSvgColor,
|
toSvgColor,
|
||||||
brightness,
|
brightness,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const {
|
|||||||
prependPrefix,
|
prependPrefix,
|
||||||
isDataUrl,
|
isDataUrl,
|
||||||
prepareNamedLogo,
|
prepareNamedLogo,
|
||||||
getSimpleIcon,
|
|
||||||
makeLogo,
|
makeLogo,
|
||||||
} = require('./logos')
|
} = require('./logos')
|
||||||
|
|
||||||
@@ -99,13 +98,6 @@ describe('Logo helpers', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(getSimpleIcon, () => {
|
|
||||||
// https://github.com/badges/shields/issues/4016
|
|
||||||
given({ name: 'get' }).expect(undefined)
|
|
||||||
// https://github.com/badges/shields/issues/4263
|
|
||||||
given({ name: 'get', color: 'blue' }).expect(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test(makeLogo, () => {
|
test(makeLogo, () => {
|
||||||
forCases([
|
forCases([
|
||||||
given('npm', { logo: 'image/svg+xml;base64,PHN2ZyB4bWxu' }),
|
given('npm', { logo: 'image/svg+xml;base64,PHN2ZyB4bWxu' }),
|
||||||
|
|||||||
15
lib/server-secrets.js
Normal file
15
lib/server-secrets.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const config = require('config').util.toObject()
|
||||||
|
|
||||||
|
const legacySecretsPath = path.join(__dirname, '..', 'private', 'secret.json')
|
||||||
|
if (fs.existsSync(legacySecretsPath)) {
|
||||||
|
console.error(
|
||||||
|
`Legacy secrets file found at ${legacySecretsPath}. It should be deleted and secrets replaced with environment variables or config/local.yml`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config.private
|
||||||
14371
package-lock.json
generated
14371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
159
package.json
159
package.json
@@ -22,45 +22,48 @@
|
|||||||
"url": "https://github.com/badges/shields"
|
"url": "https://github.com/badges/shields"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "^5.30.0",
|
"@hapi/joi": "^17.1.1",
|
||||||
"@shields_io/camp": "^18.1.1",
|
"@renovate/pep440": "^0.4.1",
|
||||||
|
"@sentry/node": "^5.24.2",
|
||||||
|
"@shields_io/camp": "^18.0.0",
|
||||||
"badge-maker": "file:badge-maker",
|
"badge-maker": "file:badge-maker",
|
||||||
"bytes": "^3.1.0",
|
"bytes": "^3.1.0",
|
||||||
"camelcase": "^6.2.0",
|
"camelcase": "^5.3.1",
|
||||||
|
"chai-as-promised": "^7.1.1",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"check-node-version": "^4.0.3",
|
"check-node-version": "^4.0.3",
|
||||||
"cloudflare-middleware": "^1.0.4",
|
"chrome-web-store-item-property": "~1.2.0",
|
||||||
"config": "^3.3.3",
|
"config": "^3.3.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.2",
|
||||||
"decamelize": "^5.0.0",
|
"decamelize": "^3.2.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"emojic": "^1.1.16",
|
"emojic": "^1.1.16",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"fast-xml-parser": "^3.17.6",
|
"fast-xml-parser": "^3.17.4",
|
||||||
|
"fsos": "^1.1.6",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"graphql": "^14.7.0",
|
"graphql": "^14.7.0",
|
||||||
"graphql-tag": "^2.11.0",
|
"graphql-tag": "^2.11.0",
|
||||||
"ioredis": "4.19.4",
|
"ioredis": "4.17.3",
|
||||||
"joi": "17.3.0",
|
"joi-extension-semver": "4.1.1",
|
||||||
"joi-extension-semver": "5.0.0",
|
"js-yaml": "^3.14.0",
|
||||||
"js-yaml": "^4.0.0",
|
"jsonpath": "~1.0.2",
|
||||||
"jsonpath": "~1.1.0",
|
|
||||||
"lodash.countby": "^4.6.0",
|
"lodash.countby": "^4.6.0",
|
||||||
"lodash.groupby": "^4.6.0",
|
"lodash.groupby": "^4.6.0",
|
||||||
"lodash.times": "^4.3.2",
|
"lodash.times": "^4.3.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.28.0",
|
||||||
"node-env-flag": "^0.1.0",
|
"node-env-flag": "^0.1.0",
|
||||||
"parse-link-header": "^1.0.1",
|
"parse-link-header": "^1.0.1",
|
||||||
"path-to-regexp": "^6.2.0",
|
"path-to-regexp": "^5.0.0",
|
||||||
"pretty-bytes": "^5.5.0",
|
"pretty-bytes": "^5.4.1",
|
||||||
"priorityqueuejs": "^2.0.0",
|
"priorityqueuejs": "^2.0.0",
|
||||||
"prom-client": "^13.0.0",
|
"prom-client": "^11.5.3",
|
||||||
"query-string": "^6.13.8",
|
"query-string": "^6.13.2",
|
||||||
"request": "~2.88.2",
|
"request": "~2.88.2",
|
||||||
"semver": "~7.3.4",
|
"semver": "~7.3.2",
|
||||||
"simple-icons": "4.6.0",
|
"simple-icons": "3.8.0",
|
||||||
"webextension-store-meta": "^1.0.3",
|
"xmldom": "~0.2.1",
|
||||||
"xmldom": "~0.4.0",
|
"xpath": "~0.0.29"
|
||||||
"xpath": "~0.0.32"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"coverage:test:core": "nyc npm run test:core",
|
"coverage:test:core": "nyc npm run test:core",
|
||||||
@@ -95,6 +98,8 @@
|
|||||||
"check-types:package": "tsd badge-maker",
|
"check-types:package": "tsd badge-maker",
|
||||||
"check-types:frontend": "tsc --noEmit --project .",
|
"check-types:frontend": "tsc --noEmit --project .",
|
||||||
"depcheck": "check-node-version --node \">= 12.0\"",
|
"depcheck": "check-node-version --node \">= 12.0\"",
|
||||||
|
"fix-issue-5294": "rimraf node_modules/@types/react-native",
|
||||||
|
"postinstall": "run-s --silent depcheck fix-issue-5294",
|
||||||
"prebuild": "run-s --silent depcheck",
|
"prebuild": "run-s --silent depcheck",
|
||||||
"features": "node scripts/export-supported-features-cli.js > supported-features.json",
|
"features": "node scripts/export-supported-features-cli.js > supported-features.json",
|
||||||
"defs": "node scripts/export-service-definitions-cli.js > service-definitions.yml",
|
"defs": "node scripts/export-service-definitions-cli.js > service-definitions.yml",
|
||||||
@@ -112,10 +117,10 @@
|
|||||||
"e2e": "start-server-and-test start http://localhost:3000 test:e2e",
|
"e2e": "start-server-and-test start http://localhost:3000 test:e2e",
|
||||||
"e2e-on-build": "cross-env CYPRESS_baseUrl=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
"e2e-on-build": "cross-env CYPRESS_baseUrl=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
||||||
"badge": "cross-env NODE_CONFIG_ENV=test TRACE_SERVICES=true node scripts/badge-cli.js",
|
"badge": "cross-env NODE_CONFIG_ENV=test TRACE_SERVICES=true node scripts/badge-cli.js",
|
||||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME"
|
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.@(js|ts|tsx)": [
|
"**/*.js": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
],
|
],
|
||||||
@@ -138,105 +143,113 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.11.6",
|
||||||
"@babel/polyfill": "^7.12.1",
|
"@babel/polyfill": "^7.11.5",
|
||||||
"@babel/register": "7.12.10",
|
"@babel/register": "7.11.5",
|
||||||
"@mapbox/react-click-to-select": "^2.2.0",
|
"@mapbox/react-click-to-select": "^2.2.0",
|
||||||
"@types/chai": "^4.2.14",
|
"@types/chai": "^4.2.12",
|
||||||
|
"@types/chai-enzyme": "^0.6.7",
|
||||||
|
"@types/enzyme": "^3.10.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/lodash.groupby": "^4.6.6",
|
"@types/lodash.groupby": "^4.6.6",
|
||||||
"@types/mocha": "^8.2.0",
|
"@types/mocha": "^8.0.3",
|
||||||
"@types/node": "^14.14.21",
|
"@types/node": "^14.11.1",
|
||||||
"@types/react-helmet": "^6.1.0",
|
"@types/react-helmet": "^6.1.0",
|
||||||
"@types/react-modal": "^3.10.6",
|
"@types/react-modal": "^3.10.6",
|
||||||
"@types/react-select": "^3.1.2",
|
"@types/react-select": "^3.0.19",
|
||||||
"@types/styled-components": "5.1.7",
|
"@types/styled-components": "5.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||||
"@typescript-eslint/parser": "^2.34.0",
|
"@typescript-eslint/parser": "^2.34.0",
|
||||||
"babel-plugin-inline-react-svg": "^1.1.2",
|
"babel-plugin-inline-react-svg": "^1.1.1",
|
||||||
"babel-plugin-istanbul": "^6.0.0",
|
"babel-plugin-istanbul": "^6.0.0",
|
||||||
"babel-preset-gatsby": "^0.5.1",
|
"babel-preset-gatsby": "^0.5.1",
|
||||||
"caller": "^1.0.1",
|
"caller": "^1.0.1",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"chai-as-promised": "^7.1.1",
|
|
||||||
"chai-datetime": "^1.7.0",
|
"chai-datetime": "^1.7.0",
|
||||||
|
"chai-enzyme": "^1.0.0-beta.1",
|
||||||
"chai-string": "^1.4.0",
|
"chai-string": "^1.4.0",
|
||||||
|
"cheerio": "^1.0.0-rc.3",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^3.1.0",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"cypress": "^6.2.1",
|
"cypress": "^5.1.0",
|
||||||
"danger": "^10.6.0",
|
"danger": "^10.4.0",
|
||||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"eslint": "^7.17.0",
|
"enzyme": "^3.11.0",
|
||||||
"eslint-config-prettier": "^7.1.0",
|
"enzyme-adapter-react-16": "^1.15.4",
|
||||||
"eslint-config-standard": "^16.0.2",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-standard-jsx": "^10.0.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"eslint-config-standard-react": "^11.0.1",
|
"eslint-config-standard": "^14.1.1",
|
||||||
|
"eslint-config-standard-react": "^9.2.0",
|
||||||
"eslint-plugin-chai-friendly": "^0.6.0",
|
"eslint-plugin-chai-friendly": "^0.6.0",
|
||||||
"eslint-plugin-cypress": "^2.11.2",
|
"eslint-plugin-cypress": "^2.11.1",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"eslint-plugin-jsdoc": "^30.7.13",
|
"eslint-plugin-jsdoc": "^30.4.2",
|
||||||
"eslint-plugin-mocha": "^8.0.0",
|
"eslint-plugin-mocha": "^6.3.0",
|
||||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.20.6",
|
||||||
"eslint-plugin-react-hooks": "^2.5.1",
|
"eslint-plugin-react-hooks": "^2.5.1",
|
||||||
"eslint-plugin-sort-class-members": "^1.9.0",
|
"eslint-plugin-sort-class-members": "^1.8.0",
|
||||||
"fetch-ponyfill": "^7.0.0",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
|
"fetch-ponyfill": "^6.1.1",
|
||||||
"form-data": "^3.0.0",
|
"form-data": "^3.0.0",
|
||||||
"gatsby": "2.30.2",
|
"fs-readfile-promise": "^3.0.1",
|
||||||
|
"gatsby": "2.24.57",
|
||||||
"gatsby-plugin-catch-links": "^2.3.10",
|
"gatsby-plugin-catch-links": "^2.3.10",
|
||||||
"gatsby-plugin-page-creator": "^2.8.0",
|
"gatsby-plugin-page-creator": "^2.3.27",
|
||||||
"gatsby-plugin-react-helmet": "^3.3.9",
|
"gatsby-plugin-react-helmet": "^3.3.9",
|
||||||
"gatsby-plugin-remove-trailing-slashes": "^2.3.10",
|
"gatsby-plugin-remove-trailing-slashes": "^2.3.10",
|
||||||
"gatsby-plugin-styled-components": "^3.3.9",
|
"gatsby-plugin-styled-components": "^3.3.9",
|
||||||
"gatsby-plugin-typescript": "^2.5.0",
|
"gatsby-plugin-typescript": "^2.4.16",
|
||||||
"got": "11.8.1",
|
"got": "11.7.0",
|
||||||
"humanize-string": "^2.1.0",
|
"humanize-string": "^2.1.0",
|
||||||
"husky": "^4.3.7",
|
"husky": "^4.3.0",
|
||||||
"icedfrisby": "4.0.0",
|
"icedfrisby": "4.0.0",
|
||||||
"icedfrisby-nock": "^2.0.0",
|
"icedfrisby-nock": "^2.0.0",
|
||||||
|
"is-png": "^2.0.0",
|
||||||
"is-svg": "^4.2.1",
|
"is-svg": "^4.2.1",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"jsdoc": "^3.6.6",
|
"jsdoc": "^3.6.5",
|
||||||
"lint-staged": "^10.5.3",
|
"lint-staged": "^10.4.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.difference": "^4.5.0",
|
"lodash.difference": "^4.5.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"mocha": "^8.2.1",
|
"mocha": "^7.2.0",
|
||||||
"mocha-env-reporter": "^4.0.0",
|
"mocha-env-reporter": "^4.0.0",
|
||||||
"mocha-junit-reporter": "^2.0.0",
|
"mocha-junit-reporter": "^2.0.0",
|
||||||
"mocha-yaml-loader": "^1.0.3",
|
"mocha-yaml-loader": "^1.0.3",
|
||||||
"nock": "13.0.5",
|
"nock": "13.0.4",
|
||||||
"node-mocks-http": "^1.10.0",
|
"node-mocks-http": "^1.9.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"opn-cli": "^5.0.0",
|
"opn-cli": "^5.0.0",
|
||||||
"portfinder": "^1.0.28",
|
"portfinder": "^1.0.28",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.1.2",
|
||||||
"react": "^16.14.0",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-error-overlay": "^6.0.8",
|
"react-error-overlay": "^6.0.7",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-modal": "^3.12.1",
|
"react-modal": "^3.11.2",
|
||||||
"react-pose": "^4.0.10",
|
"react-pose": "^4.0.10",
|
||||||
"react-select": "^3.2.0",
|
"react-select": "^3.1.0",
|
||||||
"read-all-stdin-sync": "^1.0.5",
|
"read-all-stdin-sync": "^1.0.5",
|
||||||
"redis-server": "^1.2.2",
|
"redis-server": "^1.2.2",
|
||||||
"require-hacker": "^3.0.1",
|
"require-hacker": "^3.0.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sazerac": "^2.0.0",
|
"sazerac": "^2.0.0",
|
||||||
"sinon": "^9.2.3",
|
"sinon": "^9.0.3",
|
||||||
"sinon-chai": "^3.5.0",
|
"sinon-chai": "^3.5.0",
|
||||||
"snap-shot-it": "^7.9.3",
|
"snap-shot-it": "^7.9.3",
|
||||||
"start-server-and-test": "1.11.7",
|
"start-server-and-test": "1.11.3",
|
||||||
"styled-components": "^5.2.1",
|
"styled-components": "^5.2.0",
|
||||||
"ts-mocha": "^8.0.0",
|
"tmp": "0.2.1",
|
||||||
"tsd": "^0.14.0",
|
"tsd": "^0.13.1",
|
||||||
"typescript": "^4.1.3"
|
"ts-mocha": "^7.0.0",
|
||||||
|
"typescript": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.18.3",
|
"node": "^12.18.3",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="198" height="58"><rect rx="8" x="140" width="55" height="58" fill="#555" /><g stroke="#555" stroke-width="8"><path d="M135.5 54a8 8 0 0 0 8.5 -8.5"/><rect x="4" y="4" rx="8" width="190" height="50" fill="none"/></g><g fill="#555"><path d="m23.906 33.641c.953-.083 1.906-.167 2.859-.25.108 2.099 1.511 4.139 3.578 4.722 2.438.895 5.357.799 7.559-.658 1.49-1.129 1.861-3.674.324-4.925-1.557-1.322-3.685-1.504-5.576-2.057-2.343-.565-4.912-1.133-6.611-2.979-1.805-2.088-1.627-5.485.292-7.443 2.041-2.113 5.222-2.55 8.02-2.274 2.46.244 5.058 1.343 6.252 3.635.426.908 1.095 2.241.656 3.108-.888.173-1.81.148-2.715.245-.077-2.084-1.727-4.073-3.863-4.234-1.902-.317-4.02-.252-5.691.802-1.398.989-1.849 3.363-.381 4.494 1.281 1.01 2.962 1.199 4.482 1.642 2.66.627 5.602 1.118 7.596 3.158 2 2.188 1.893 5.84-.088 8.01-2.01 2.32-5.304 2.972-8.237 2.713-2.585-.147-5.319-1.024-6.916-3.184-.987-1.288-1.517-2.905-1.542-4.523"/><path d="m45.953 41c0-7.635 0-15.271 0-22.906.938 0 1.875 0 2.813 0 0 2.74 0 5.479 0 8.219 1.391-1.721 3.69-2.523 5.86-2.236 1.975.154 4.03 1.371 4.513 3.402.504 1.973.278 4.02.33 6.04 0 2.495 0 4.989 0 7.484-.938 0-1.875 0-2.813 0-.009-3.675.018-7.351-.014-11.03-.026-1.342-.627-2.835-2-3.282-2.187-.802-5.077.393-5.609 2.773-.417 1.764-.216 3.586-.264 5.381 0 2.051 0 4.102 0 6.153-.938 0-1.875 0-2.813 0"/><path d="m63.781 21.328v-3.234h2.813v3.234zm0 19.672v-16.594h2.813v16.594z"/><path d="m82.25 35.656c.969.12 1.938.24 2.906.359-.702 3.464-4.348 5.767-7.781 5.386-3.235-.066-6.43-2.328-7.06-5.598-.843-3.307-.404-7.285 2.101-9.784 3.082-3 8.699-2.618 11.235.892 1.374 1.85 1.676 4.267 1.578 6.51-4.125 0-8.25 0-12.375 0-.142 2.889 2.267 6 5.346 5.658 1.881-.162 3.613-1.566 4.045-3.423m-9.234-4.547c3.089 0 6.177 0 9.266 0 .129-2.774-2.616-5.422-5.419-4.713-2.174.427-3.912 2.474-3.846 4.713"/><path d="m88.64 41v-22.906h2.813v22.906z"/><path d="m106.59 41c0-.698 0-1.396 0-2.094-1.412 2.442-4.776 3.067-7.233 1.949-2.378-1.02-3.971-3.403-4.345-5.924-.507-2.761-.123-5.768 1.389-8.167 1.863-2.705 5.968-3.642 8.711-1.741.422.228 1.028 1.144 1.294 1.018-.006-2.649-.0001-5.298-.003-7.948.932 0 1.865 0 2.797 0 0 7.635 0 15.271 0 22.906-.87 0-1.74 0-2.61 0m-8.89-8.281c-.075 2.246.637 4.861 2.79 5.952 2 1.023 4.682-.047 5.488-2.134.897-1.996.746-4.278.388-6.382-.425-1.95-2.046-3.804-4.158-3.805-1.903-.065-3.633 1.363-4.099 3.181-.327 1.028-.394 2.116-.408 3.188"/><path d="m112.52 36.05c.927-.146 1.854-.292 2.781-.438.126 1.69 1.513 3.244 3.239 3.365 1.398.212 3.01.12 4.12-.851.807-.749 1.1-2.243.159-3.01-.908-.723-2.115-.812-3.182-1.172-1.797-.485-3.713-.848-5.243-1.97-1.83-1.551-1.868-4.679-.099-6.293 1.577-1.507 3.918-1.784 6-1.594 1.685.176 3.54.749 4.535 2.217.464.715.708 1.549.844 2.384-.917.125-1.833.25-2.75.375-.121-1.569-1.653-2.762-3.19-2.695-1.246-.082-2.702.012-3.608.982-.624.724-.543 1.971.314 2.481.998.706 2.269.757 3.389 1.173 1.754.512 3.647.848 5.141 1.965 1.686 1.476 1.728 4.244.396 5.966-1.298 1.788-3.597 2.417-5.709 2.448-1.466-.007-2.984-.214-4.299-.893-1.599-.909-2.585-2.655-2.84-4.444"/></g><g fill="#fff"><path d="m151.11 41v-22.906h3.03v22.906z"/><path d="m158.55 29.844c-.277-4.765 2.335-9.977 7.05-11.551 4.902-1.757 11.226.197 13.477 5.098 2.266 4.706 1.89 10.92-1.767 14.833-4.554 4.948-13.81 3.976-17.08-1.954-1.111-1.946-1.679-4.188-1.68-6.426m3.125.047c-.377 4.273 2.892 8.844 7.375 8.951 3.791.221 7.557-2.653 7.997-6.497.794-3.731.139-8.292-3.107-10.696-3.788-2.814-10.05-1.104-11.591 3.444-.54 1.539-.642 3.181-.675 4.798"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -9,4 +9,4 @@ const definitions = collectDefinitions()
|
|||||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||||
const cleaned = JSON.parse(JSON.stringify(definitions))
|
const cleaned = JSON.parse(JSON.stringify(definitions))
|
||||||
|
|
||||||
process.stdout.write(yaml.dump(cleaned, { flowLevel: 5 }))
|
process.stdout.write(yaml.safeDump(cleaned, { flowLevel: 5 }))
|
||||||
|
|||||||
50
scripts/import-github-tokens.js
Normal file
50
scripts/import-github-tokens.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { promises: fs } = require('fs')
|
||||||
|
const Redis = require('ioredis')
|
||||||
|
|
||||||
|
const key = 'githubUserTokens'
|
||||||
|
|
||||||
|
async function loadTokens() {
|
||||||
|
const contents = await fs.readFile('all_tokens_uniq.json', 'utf8')
|
||||||
|
const tokens = JSON.parse(contents)
|
||||||
|
console.log(`${tokens.length} tokens loaded`)
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
const redis = new Redis(process.env.REDIS_URL, {
|
||||||
|
tls: { servername: new URL(process.env.REDIS_URL).hostname },
|
||||||
|
})
|
||||||
|
redis.on('error', err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
return redis
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const redis = createClient()
|
||||||
|
const tokens = await loadTokens()
|
||||||
|
await redis.sadd(key, tokens)
|
||||||
|
await redis.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function list() {
|
||||||
|
const redis = createClient()
|
||||||
|
const tokens = await redis.smembers(key)
|
||||||
|
console.log(`${tokens.length} tokens loaded`)
|
||||||
|
await redis.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
// await load()
|
||||||
|
await list()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Appease the linter.
|
||||||
|
module.exports = { load, list }
|
||||||
@@ -5,6 +5,7 @@ console.log(config)
|
|||||||
const GithubConstellation = require('../services/github/github-constellation')
|
const GithubConstellation = require('../services/github/github-constellation')
|
||||||
|
|
||||||
const { persistence } = new GithubConstellation({
|
const { persistence } = new GithubConstellation({
|
||||||
|
persistence: config.public.persistence,
|
||||||
service: config.public.services.github,
|
service: config.public.services.github,
|
||||||
private: config.private,
|
private: config.private,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
|
|
||||||
|
|
||||||
# Start off less strict to work around various nvm errors.
|
|
||||||
set -e
|
|
||||||
export NVM_DIR="/opt/circleci/.nvm"
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
|
|
||||||
nvm install $NODE_VERSION
|
|
||||||
nvm use $NODE_VERSION
|
|
||||||
|
|
||||||
# Stricter.
|
|
||||||
set -euo pipefail
|
|
||||||
node --version
|
|
||||||
|
|
||||||
# Install the shields.io dependencies.
|
|
||||||
if [[ "$NODE_VERSION" == "v10" ]]; then
|
|
||||||
# Avoid a depcheck error.
|
|
||||||
npm ci --ignore-scripts
|
|
||||||
else
|
|
||||||
npm ci
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the package tests.
|
|
||||||
npm run test:package
|
|
||||||
npm run check-types:package
|
|
||||||
|
|
||||||
# Delete the shields.io dependencies.
|
|
||||||
rm -rf node_modules/
|
|
||||||
|
|
||||||
# Run a smoke test (render a badge with the CLI) with only the package
|
|
||||||
# dependencies installed.
|
|
||||||
cd badge-maker
|
|
||||||
npm link
|
|
||||||
badge cactus grown :green @flat
|
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
// Set up Sentry reporting as early in the process as possible.
|
// Set up Sentry reporting as early in the process as possible.
|
||||||
const config = require('config').util.toObject()
|
const config = require('config').util.toObject()
|
||||||
const Sentry = require('@sentry/node')
|
const Sentry = require('@sentry/node')
|
||||||
@@ -35,13 +37,6 @@ if (process.argv[3]) {
|
|||||||
console.log('Configuration:')
|
console.log('Configuration:')
|
||||||
console.dir(config.public, { depth: null })
|
console.dir(config.public, { depth: null })
|
||||||
|
|
||||||
if (fs.existsSync('.env')) {
|
|
||||||
console.error(
|
|
||||||
'Legacy .env file found. It should be deleted and replaced with environment variables or config/local.yml'
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacySecretsPath = path.join(__dirname, 'private', 'secret.json')
|
const legacySecretsPath = path.join(__dirname, 'private', 'secret.json')
|
||||||
if (fs.existsSync(legacySecretsPath)) {
|
if (fs.existsSync(legacySecretsPath)) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Joi = require('joi')
|
const Joi = require('@hapi/joi')
|
||||||
const { nonNegativeInteger } = require('../validators')
|
const { nonNegativeInteger } = require('../validators')
|
||||||
const { BaseJsonService } = require('..')
|
const { BaseJsonService } = require('..')
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const t = (module.exports = new ServiceTester({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
t.create('Weekly Downloads')
|
t.create('Weekly Downloads')
|
||||||
.get('/dw/duckduckgo-for-firefox.json')
|
.get('/dw/dustman.json')
|
||||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||||
|
|
||||||
t.create('Weekly Downloads (not found)')
|
t.create('Weekly Downloads (not found)')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user