diff --git a/README.md b/README.md index 37f3e50..047e94e 100644 --- a/README.md +++ b/README.md @@ -225,13 +225,14 @@ import { fetchSponsors } from '@lizardbyte/contribkit' const sponsors = await fetchSponsors({ github: { - token, login, }, // ... }) ``` +Set credentials with environment variables such as `CONTRIBKIT_GITHUB_TOKEN`. + Check the type definition or source code for more utils available. ### Renderers diff --git a/src/configs/credentials.ts b/src/configs/credentials.ts new file mode 100644 index 0000000..caee0cd --- /dev/null +++ b/src/configs/credentials.ts @@ -0,0 +1,83 @@ +import type { ContribkitConfig } from '../types' +import process from 'node:process' +import dotenv from 'dotenv' + +export interface EnvCredentials { + github?: { + token?: string + } + patreon?: { + token?: string + } + opencollective?: { + key?: string + } + afdian?: { + token?: string + } + polar?: { + token?: string + } + githubContributors?: { + token?: string + } + gitlabContributors?: { + token?: string + } + githubContributions?: { + token?: string + } +} + +interface ConfigWithCredentials extends ContribkitConfig { + credentials?: EnvCredentials +} + +export function pruneUndefined(value: T): T { + if (Array.isArray(value)) + return value.map(item => pruneUndefined(item)) as T + + if (!value || typeof value !== 'object') + return value + + return Object.fromEntries( + Object.entries(value) + .filter(([, item]) => item !== undefined) + .map(([key, item]) => [key, pruneUndefined(item)]), + ) as T +} + +export function loadEnvCredentials(): EnvCredentials { + dotenv.config({ quiet: true }) + + return pruneUndefined({ + github: { + token: process.env.CONTRIBKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN, + }, + patreon: { + token: process.env.CONTRIBKIT_PATREON_TOKEN || process.env.PATREON_TOKEN, + }, + opencollective: { + key: process.env.CONTRIBKIT_OPENCOLLECTIVE_KEY || process.env.OPENCOLLECTIVE_KEY, + }, + afdian: { + token: process.env.CONTRIBKIT_AFDIAN_TOKEN || process.env.AFDIAN_TOKEN, + }, + polar: { + token: process.env.CONTRIBKIT_POLAR_TOKEN || process.env.POLAR_TOKEN, + }, + githubContributors: { + token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN, + }, + gitlabContributors: { + token: process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN, + }, + githubContributions: { + token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN, + }, + }) +} + +export function getCredentials(config: ContribkitConfig): EnvCredentials { + return (config as ConfigWithCredentials).credentials ?? loadEnvCredentials() +} diff --git a/src/configs/env.ts b/src/configs/env.ts index 5a9a151..d3b9aff 100644 --- a/src/configs/env.ts +++ b/src/configs/env.ts @@ -1,29 +1,23 @@ import type { ContribkitConfig, GitHubAccountType } from '../types' import process from 'node:process' import dotenv from 'dotenv' +import { loadEnvCredentials, pruneUndefined } from './credentials' -function getDeprecatedEnv(name: string, replacement: string) { - const value = process.env[name] - if (value) - console.warn(`[contribkit] env.${name} is deprecated, use env.${replacement} instead`) - return value +export interface EnvConfig { + config: Partial + credentials: ReturnType } -export function loadEnv(): Partial { +export function loadEnv(): EnvConfig { dotenv.config({ quiet: true }) const config: Partial = { mode: process.env.CONTRIBKIT_MODE as ContribkitConfig['mode'] | undefined, github: { login: process.env.CONTRIBKIT_GITHUB_LOGIN || process.env.GITHUB_LOGIN, - token: process.env.CONTRIBKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN, type: (process.env.CONTRIBKIT_GITHUB_TYPE || process.env.GITHUB_TYPE) as GitHubAccountType | undefined, }, - patreon: { - token: process.env.CONTRIBKIT_PATREON_TOKEN || process.env.PATREON_TOKEN, - }, opencollective: { - key: process.env.CONTRIBKIT_OPENCOLLECTIVE_KEY || process.env.OPENCOLLECTIVE_KEY, id: process.env.CONTRIBKIT_OPENCOLLECTIVE_ID || process.env.OPENCOLLECTIVE_ID, slug: process.env.CONTRIBKIT_OPENCOLLECTIVE_SLUG || process.env.OPENCOLLECTIVE_SLUG, githubHandle: process.env.CONTRIBKIT_OPENCOLLECTIVE_GH_HANDLE || process.env.OPENCOLLECTIVE_GH_HANDLE, @@ -31,11 +25,9 @@ export function loadEnv(): Partial { }, afdian: { userId: process.env.CONTRIBKIT_AFDIAN_USER_ID || process.env.AFDIAN_USER_ID, - token: process.env.CONTRIBKIT_AFDIAN_TOKEN || process.env.AFDIAN_TOKEN, exchangeRate: Number.parseFloat(process.env.CONTRIBKIT_AFDIAN_EXCHANGE_RATE || process.env.AFDIAN_EXCHANGE_RATE || '0') || undefined, }, polar: { - token: process.env.CONTRIBKIT_POLAR_TOKEN || process.env.POLAR_TOKEN, organization: process.env.CONTRIBKIT_POLAR_ORGANIZATION || process.env.POLAR_ORGANIZATION, }, liberapay: { @@ -44,12 +36,10 @@ export function loadEnv(): Partial { outputDir: process.env.CONTRIBKIT_DIR, githubContributors: { login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN, - token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN, minContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN) || 1, repo: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO, }, gitlabContributors: { - token: process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN, minContributions: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_MIN) || 1, repoId: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_REPO_ID), }, @@ -60,12 +50,14 @@ export function loadEnv(): Partial { }, githubContributions: { login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN, - token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN, maxContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_MAX) || undefined, logarithmicScaling: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGARITHMIC === 'true', }, } // remove undefined keys - return JSON.parse(JSON.stringify(config)) + return { + config: pruneUndefined(config), + credentials: loadEnvCredentials(), + } } diff --git a/src/configs/index.ts b/src/configs/index.ts index f690982..d7d5250 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -14,6 +14,7 @@ export function defineConfig(config: ContribkitConfig): ContribkitConfig { export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise> { const env = loadEnv() + const envConfig = env.config const { config = {} } = await _loadConfig({ sources: [ @@ -39,35 +40,36 @@ export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise if (!['sponsors', 'sponsees'].includes(resolved.mode)) throw new Error(`Invalid mode: ${resolved.mode}. Expected "sponsors" or "sponsees".`) - resolved.name = inlineConfig.name || config.name || env.name || resolved.mode + resolved.name = inlineConfig.name || config.name || envConfig.name || resolved.mode return resolved } diff --git a/src/providers/afdian.ts b/src/providers/afdian.ts index 2c47edc..3dc915e 100644 --- a/src/providers/afdian.ts +++ b/src/providers/afdian.ts @@ -1,6 +1,7 @@ import type { ContribkitConfig, Provider, Sponsorship } from '../types' import { createHash } from 'node:crypto' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' // afdian api docs https://afdian.net/p/9c65d9cc617011ed81c352540025c377 @@ -12,14 +13,13 @@ export const AfdianProvider: Provider = { return Promise.resolve([]) } - return fetchAfdianSponsors(config.afdian) + return fetchAfdianSponsors(config.afdian, getCredentials(config).afdian?.token) }, } -export async function fetchAfdianSponsors(options: ContribkitConfig['afdian'] = {}): Promise { +export async function fetchAfdianSponsors(options: ContribkitConfig['afdian'] = {}, token = getCredentials({}).afdian?.token): Promise { const { userId, - token, exchangeRate = 6.5, includePurchases = true, purchaseEffectivity = 30, diff --git a/src/providers/crowdinContributors.ts b/src/providers/crowdinContributors.ts index 90c7c79..7bf7ed2 100644 --- a/src/providers/crowdinContributors.ts +++ b/src/providers/crowdinContributors.ts @@ -14,7 +14,7 @@ export const CrowdinContributorsProvider: Provider = { name: 'crowdinContributors', fetchSponsors(config) { return fetchCrowdinContributors( - config.crowdinContributors?.token || config.token!, + config.crowdinContributors?.token, config.crowdinContributors?.projectId || 0, config.crowdinContributors?.minTranslations || 1, ) @@ -22,7 +22,7 @@ export const CrowdinContributorsProvider: Provider = { } export async function fetchCrowdinContributors( - token: string, + token: string | undefined, projectId: number, minTranslations = 1, ): Promise { diff --git a/src/providers/github.ts b/src/providers/github.ts index dcf6c1c..449a061 100644 --- a/src/providers/github.ts +++ b/src/providers/github.ts @@ -1,5 +1,6 @@ import type { ContribkitConfig, GitHubAccountType, Provider, Sponsorship, Tier } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' import { normalizeUrl } from '../utils' function getMonthDifference(startDate: Date, endDate: Date) { @@ -42,15 +43,15 @@ export const GitHubProvider: Provider = { fetchSponsors(config) { if (config.mode === 'sponsees') { return fetchGitHubSponsoringAsSponsorships( - config.github?.token || config.token!, - config.github?.login || config.login!, + getCredentials(config).github?.token, + config.github?.login, config.github?.type || 'user', ) } return fetchGitHubSponsors( - config.github?.token || config.token!, - config.github?.login || config.login!, + getCredentials(config).github?.token, + config.github?.login, config.github?.type || 'user', config, ) @@ -58,8 +59,8 @@ export const GitHubProvider: Provider = { } export async function fetchGitHubSponsors( - token: string, - login: string, + token: string | undefined, + login: string | undefined, type: GitHubAccountType, config: ContribkitConfig, ): Promise { @@ -360,10 +361,15 @@ export async function fetchGitHubSponsoring( } export async function fetchGitHubSponsoringAsSponsorships( - token: string, - login: string, + token: string | undefined, + login: string | undefined, type: GitHubAccountType, ): Promise { + if (!token) + throw new Error('GitHub token is required') + if (!login) + throw new Error('GitHub login is required') + // Sponsees mode always loads full history regardless of active status. const records = await fetchGitHubSponsoring(token, login, type, false) const recordsBySponsorable = groupSponsoringRecordsByLogin(records) diff --git a/src/providers/githubContributions.ts b/src/providers/githubContributions.ts index 262bbab..95afa8b 100644 --- a/src/providers/githubContributions.ts +++ b/src/providers/githubContributions.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' export const GitHubContributionsProvider: Provider = { name: 'githubContributions', @@ -8,7 +9,7 @@ export const GitHubContributionsProvider: Provider = { throw new Error('GitHub login is required for githubContributions provider') return fetchGitHubContributions( - config.githubContributions?.token || config.token!, + getCredentials(config).githubContributions?.token, config.githubContributions.login, config.githubContributions.maxContributions, config.githubContributions.logarithmicScaling, @@ -304,7 +305,7 @@ function convertToSponsorships( } export async function fetchGitHubContributions( - token: string, + token: string | undefined, login: string, maxContributions?: number, logarithmicScaling?: boolean, diff --git a/src/providers/githubContributors.ts b/src/providers/githubContributors.ts index be170b0..5ee56a3 100644 --- a/src/providers/githubContributors.ts +++ b/src/providers/githubContributors.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' export const GitHubContributorsProvider: Provider = { name: 'githubContributors', @@ -8,8 +9,8 @@ export const GitHubContributorsProvider: Provider = { throw new Error('GitHub repository is required') return fetchGitHubContributors( - config.githubContributors?.token || config.token!, - config.githubContributors?.login || config.login!, + getCredentials(config).githubContributors?.token, + config.githubContributors.login, config.githubContributors.repo, config.githubContributors?.minContributions, ) @@ -17,8 +18,8 @@ export const GitHubContributorsProvider: Provider = { } export async function fetchGitHubContributors( - token: string, - login: string, + token: string | undefined, + login: string | undefined, repo: string, minContributions = 1, ): Promise { diff --git a/src/providers/gitlabContributors.ts b/src/providers/gitlabContributors.ts index 33d5c4b..92cf637 100644 --- a/src/providers/gitlabContributors.ts +++ b/src/providers/gitlabContributors.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' interface GitLabContributor { name: string @@ -22,7 +23,7 @@ export const GitlabContributorsProvider: Provider = { throw new Error('Gitlab repoId is required') return fetchGitlabContributors( - config.gitlabContributors?.token || config.token!, + getCredentials(config).gitlabContributors?.token, config.gitlabContributors.repoId, config.gitlabContributors?.minContributions, ) @@ -30,7 +31,7 @@ export const GitlabContributorsProvider: Provider = { } export async function fetchGitlabContributors( - token: string, + token: string | undefined, repoId: number, minContributions: number = 1, ): Promise { diff --git a/src/providers/index.ts b/src/providers/index.ts index 613dd39..db10d17 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,4 +1,5 @@ import type { ContribkitConfig, Provider, ProviderName } from '../types' +import { getCredentials } from '../configs/credentials' import { AfdianProvider } from './afdian' import { CrowdinContributorsProvider } from './crowdinContributors' import { GitHubProvider } from './github' @@ -27,31 +28,32 @@ export const ProvidersMap = { export function guessProviders(config: ContribkitConfig) { const items: ProviderName[] = [] + const credentials = getCredentials(config) if (config.github && config.github.login) items.push('github') - if (config.patreon && config.patreon.token) + if (credentials.patreon?.token) items.push('patreon') if (config.opencollective && (config.opencollective.id || config.opencollective.slug || config.opencollective.githubHandle)) items.push('opencollective') - if (config.afdian && config.afdian.userId && config.afdian.token) + if (config.afdian?.userId && credentials.afdian?.token) items.push('afdian') - if (config.polar && config.polar.token) + if (credentials.polar?.token) items.push('polar') if (config.liberapay && config.liberapay.login) items.push('liberapay') - if (config.githubContributors?.login && config.githubContributors?.token) + if (config.githubContributors?.login && credentials.githubContributors?.token) items.push('githubContributors') - if (config.githubContributions?.login && config.githubContributions?.token) + if (config.githubContributions?.login && credentials.githubContributions?.token) items.push('githubContributions') - if (config.gitlabContributors?.token && config.gitlabContributors?.repoId) + if (credentials.gitlabContributors?.token && config.gitlabContributors?.repoId) items.push('gitlabContributors') if (config.crowdinContributors?.token && config.crowdinContributors?.projectId) diff --git a/src/providers/opencollective.ts b/src/providers/opencollective.ts index 373bed7..10a2f93 100644 --- a/src/providers/opencollective.ts +++ b/src/providers/opencollective.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' import { normalizeUrl } from '../utils' interface SocialLink { @@ -13,7 +14,7 @@ export const OpenCollectiveProvider: Provider = { if (config.mode === 'sponsees') { const slug = config.opencollective?.slug || config.opencollective?.id return fetchOpenCollectiveSponsors( - config.opencollective?.key, + getCredentials(config).opencollective?.key, undefined, slug, config.opencollective?.githubHandle, @@ -23,7 +24,7 @@ export const OpenCollectiveProvider: Provider = { } return fetchOpenCollectiveSponsors( - config.opencollective?.key, + getCredentials(config).opencollective?.key, config.opencollective?.id, config.opencollective?.slug, config.opencollective?.githubHandle, diff --git a/src/providers/patreon.ts b/src/providers/patreon.ts index f165915..b112680 100644 --- a/src/providers/patreon.ts +++ b/src/providers/patreon.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { $fetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' export const PatreonProvider: Provider = { name: 'patreon', @@ -9,11 +10,11 @@ export const PatreonProvider: Provider = { return Promise.resolve([]) } - return fetchPatreonSponsors(config.patreon?.token || config.token!) + return fetchPatreonSponsors(getCredentials(config).patreon?.token) }, } -export async function fetchPatreonSponsors(token: string): Promise { +export async function fetchPatreonSponsors(token: string | undefined): Promise { if (!token) throw new Error('Patreon token is required') diff --git a/src/providers/polar.ts b/src/providers/polar.ts index 8ea687a..97fddaa 100644 --- a/src/providers/polar.ts +++ b/src/providers/polar.ts @@ -1,5 +1,6 @@ import type { Provider, Sponsorship } from '../types' import { ofetch } from 'ofetch' +import { getCredentials } from '../configs/credentials' export const PolarProvider: Provider = { name: 'polar', @@ -9,11 +10,11 @@ export const PolarProvider: Provider = { return Promise.resolve([]) } - return fetchPolarSponsors(config.polar?.token || config.token!, config.polar?.organization) + return fetchPolarSponsors(getCredentials(config).polar?.token, config.polar?.organization) }, } -export async function fetchPolarSponsors(token: string, organization?: string): Promise { +export async function fetchPolarSponsors(token: string | undefined, organization?: string): Promise { if (!token) throw new Error('Polar token is required') if (!organization) diff --git a/src/types.ts b/src/types.ts index 96392fe..a85df63 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,14 +88,6 @@ export interface ProvidersConfig { * Will read from `CONTRIBKIT_GITHUB_LOGIN` environment variable if not set. */ login?: string - /** - * GitHub Token that have access to your sponsorships. - * - * Will read from `CONTRIBKIT_GITHUB_TOKEN` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * The account type for sponsorships. * @@ -104,25 +96,8 @@ export interface ProvidersConfig { */ type?: GitHubAccountType } - patreon?: { - /** - * Patreon Token that have access to your sponsorships. - * - * Will read from `CONTRIBKIT_PATREON_TOKEN` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string - } + patreon?: Record opencollective?: { - /** - * Api key of your OpenCollective account. - * - * Will read from `CONTRIBKIT_OPENCOLLECTIVE_KEY` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - key?: string /** * The id of your account. * @@ -157,15 +132,6 @@ export interface ProvidersConfig { * @see https://afdian.net/dashboard/dev */ userId?: string - /** - * Afdian Token that have access to your sponsorships. - * - * Will read from `CONTRIBKIT_AFDIAN_TOKEN` environment variable if not set. - * - * @see https://afdian.net/dashboard/dev - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * Exchange rate of USD to CNY * @@ -185,15 +151,6 @@ export interface ProvidersConfig { } polar?: { - /** - * Polar token that have access to your sponsorships. - * - * Will read from `CONTRIBKIT_POLAR_TOKEN` environment variable if not set. - * - * @see https://polar.sh/settings - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * The name of the organization to fetch sponsorships from. If not set, it will fetch the sponsorships of the user. * @@ -218,14 +175,6 @@ export interface ProvidersConfig { * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN` environment variable if not set. */ login?: string - /** - * GitHub Token that have access to your sponsorships. - * - * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * The minimum number of contributions to be considered a sponsor. * @@ -241,14 +190,6 @@ export interface ProvidersConfig { } gitlabContributors?: { - /** - * Gitlab Token that have access contributors. - * - * Will read from `CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * The minimum number of contributions to be considered a sponsor. * @@ -291,14 +232,6 @@ export interface ProvidersConfig { * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN` environment variable if not set. */ login?: string - /** - * GitHub Token that has access to read user contributions. - * - * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN` environment variable if not set. - * - * @deprecated It's not recommended set this value directly, pass from env or use `.env` file. - */ - token?: string /** * Cap the maximum contribution count per organization/user. * Useful to prevent one dominant contributor from overshadowing others in visualizations. @@ -425,16 +358,6 @@ export interface ContribkitConfig extends ProvidersConfig, ContribkitRenderOptio */ mode?: SponsorshipMode - /** - * @deprecated use `github.login` instead - */ - login?: string - - /** - * @deprecated use `github.token` instead - */ - token?: string - /** * @default auto detect based on the config provided */