Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 38 additions & 45 deletions packages/aws-lambda/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/core/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/fastify/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/fetch/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/node/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/peer/README.md

Large diffs are not rendered by default.

83 changes: 38 additions & 45 deletions packages/shared/README.md

Large diffs are not rendered by default.

87 changes: 66 additions & 21 deletions scripts/sync-sponsors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ interface Sponsor {
amount: number
createdAt: string
tierTitle: string
tierTitlePlural: string
tierLevel: number
/** Extra rel tokens for the sponsor's link (e.g. `sponsored`); may be empty. */
rel: string
link: string
org: boolean
sidebarSize?: string
sidebarLogo?: string
/** Tagline, present on sponsors that bought an ad slot. */
description?: string
/** Brand tint behind the ad card, both themes required together. */
background?: { light: string, dark: string }
/** 1-based ad-grid position the sponsor bought. */
slot?: number
[key: string]: unknown
}

const SPONSORS_SOURCE_URL = 'https://raw.githubusercontent.com/middleapi/static/refs/heads/main/sponsors.json'
const PAST_SPONSORS_URL = 'https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg'
const ROOT_DIR = process.cwd()
const README_FILE_NAME = 'README.md'

Expand Down Expand Up @@ -47,17 +54,9 @@ async function findReadmes(dir: string): Promise<string[]> {
return result.concat(...subResults)
}

function withTracking(url: string): string {
try {
const tracked = new URL(url)

tracked.searchParams.set('ref', 'orpc')

return tracked.toString()
}
catch {
return url
}
/** `noopener` always, plus whatever extra tokens the data carries. */
function relAttribute(sponsor: Sponsor): string {
return ['noopener', sponsor.rel].filter(Boolean).join(' ')
}

function escapeHtml(value: string): string {
Expand All @@ -77,13 +76,48 @@ function getTierImageSizeAndColumns(tierLevel: number, tierLevels: number[]): [c
return [column, Math.floor(838 / column)]
}

/**
* The slot sponsors' cards, mirroring the docs ad cards — logo on the left,
* name over tagline on the right — but without the brand backgrounds (GitHub
* strips inline styles anyway). One card per row. The oversized cell width is
* a preferred width, not a minimum: GitHub caps tables at the container, so
* the card spans the full row on any screen and the text wraps on phones
* instead of scrolling. One cell per card, the logo floated with the
* deprecated-but-sanitizer-safe `align`/`hspace` attributes, so no table
* border cuts through the card.
*/
function buildSlotCards(slotSponsors: Sponsor[]): string[] {
const lines = ['<table>']

for (const sponsor of slotSponsors) {
const name = escapeHtml(sponsor.name ?? sponsor.login)
const description = escapeHtml(sponsor.description ?? '')

lines.push(' <tr>')
lines.push(` <td width="2000"><a href="${escapeHtml(sponsor.link)}" target="_blank" rel="${relAttribute(sponsor)}" title="${description}"><img src="${escapeHtml(sponsor.avatar)}" width="64" align="left" hspace="12" alt="${name}"/><b>${name}</b></a><br /><sub>${description}</sub></td>`)
lines.push(' </tr>')
}

lines.push('</table>')
lines.push('')

return lines
}

function buildSponsorsSection(sponsors: Sponsor[]): string {
const activeSponsors = sponsors.filter(sponsor => sponsor.tierLevel > 0 && sponsor.amount > 0)
const pastSponsors = sponsors.filter(sponsor => sponsor.tierLevel <= 0 || sponsor.amount <= 0)

// Slot sponsors are featured as cards up top; the tier tables below carry
// everyone else so nobody appears twice.
const slotSponsors = activeSponsors
.filter(sponsor => sponsor.slot !== undefined)
.sort((a, b) => a.slot! - b.slot!)
const tieredSponsors = activeSponsors.filter(sponsor => sponsor.slot === undefined)

const groupedSponsors = new Map<number, Sponsor[]>()

for (const sponsor of activeSponsors) {
for (const sponsor of tieredSponsors) {
const group = groupedSponsors.get(sponsor.tierLevel)

if (group) {
Expand All @@ -101,6 +135,13 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
'',
]

if (slotSponsors.length > 0) {
lines.push(...buildSlotCards(slotSponsors))
}

// Sizes rank against every active tier, slot sponsors' tiers included, so
// featuring the top tiers as cards does not inflate the tables below them.
const sizeTierLevels = [...new Set(activeSponsors.map(sponsor => sponsor.tierLevel))].sort((a, b) => b - a)
const tierLevels = [...groupedSponsors.keys()].sort((a, b) => b - a)

for (const tierLevel of tierLevels) {
Expand All @@ -110,8 +151,8 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
continue
}

const tierTitle = tierSponsors[0]?.tierTitle ?? `Tier ${tierLevel}`
const [columns, imageSize] = getTierImageSizeAndColumns(tierLevel, tierLevels)
const tierTitle = tierSponsors[0]?.tierTitlePlural ?? `Tier ${tierLevel}`
const [columns, imageSize] = getTierImageSizeAndColumns(tierLevel, sizeTierLevels)

lines.push(`### ${tierTitle}`)
lines.push('')
Expand All @@ -123,7 +164,7 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
const displayName = sponsor.name ?? sponsor.login
const escapedName = escapeHtml(displayName)

lines.push(` <td align="center"><a href="${escapeHtml(href)}" target="_blank" rel="sponsored noopener" title="${escapedName}"><img src="${escapeHtml(sponsor.avatar)}" width="${imageSize}" alt="${escapedName}"/><br />${escapedName}</a></td>`)
lines.push(` <td align="center"><a href="${escapeHtml(href)}" target="_blank" rel="${relAttribute(sponsor)}" title="${escapedName}"><img src="${escapeHtml(sponsor.avatar)}" width="${imageSize}" alt="${escapedName}"/><br />${escapedName}</a></td>`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: sponsor.rel is interpolated into the rel="..." attribute unescaped, unlike every other trusted-but-escaped field on this line (and the card site on L97). If a sponsor's rel ever carried a ", it would break out of the attribute. Wrap the value with the existing escapeHtml helper for consistency with the rest of the templated line.


const isRowEnd = (index + 1) % columns === 0
const isLast = index === tierSponsors.length - 1
Expand All @@ -142,7 +183,7 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
if (pastSponsors.length > 0) {
const noun = pastSponsors.length === 1 ? 'past sponsor' : 'past sponsors'

lines.push(`With thanks to ${pastSponsors.length} ${noun} who helped get us here.`)
lines.push(`With thanks to [${pastSponsors.length} ${noun}](${PAST_SPONSORS_URL}) who helped get us here.`)
lines.push('')
}

Expand All @@ -160,7 +201,10 @@ function replaceSponsorsSection(content: string, replacement: string): string {
const nextHeadingIndex = content.indexOf('\n## ', startIndex + heading.length)
const endIndex = nextHeadingIndex === -1 ? content.length : nextHeadingIndex + 1

return `${content.slice(0, startIndex)}${replacement}${content.slice(endIndex)}`
// Trimmed to a single trailing newline, or a section replaced at the end of
// the file leaves a blank last line that eslint's markdown fixer removes —
// and the next sync would put back, forever.
return `${`${content.slice(0, startIndex)}${replacement}${content.slice(endIndex)}`.trimEnd()}\n`
}

async function main(): Promise<void> {
Expand All @@ -170,7 +214,8 @@ async function main(): Promise<void> {
throw new Error(`Failed to fetch sponsors data: ${response.status} ${response.statusText}`)
}

const sponsors = (await response.json() as Sponsor[]).map(sponsor => ({ ...sponsor, link: withTracking(sponsor.link) }))
// Links arrive with their tracking params already baked in upstream.
const sponsors = await response.json() as Sponsor[]
const readmeFiles = await findReadmes(ROOT_DIR)
const replacement = buildSponsorsSection(sponsors)

Expand Down
Loading
Loading