first commit

This commit is contained in:
2025-04-14 19:27:23 +08:00
commit c27d535c02
18 changed files with 3336 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
name: update-continew-admin-contributors-svg
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
# Schedule the interval of the checks.
schedule:
- cron: 00 17 * * *
jobs:
update-svg:
name: Update contributors SVG
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 7
- name: Set node version to 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
cache: "pnpm"
- name: Install deps
run: pnpm install
- name: Run generation script
run: pnpm tsx src/main.ts -t ${{ secrets.TOKEN }} -o continew-org -r continew-admin
- name: Update image
run: |
git config user.email "charles7c@126.com"
git config user.name "Charles7c"
git add .
git commit -m "workflow: update image" && git push origin main || exit 0

View File

@@ -0,0 +1,62 @@
name: update-continew-contributors-svg
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
# Schedule the interval of the checks.
schedule:
- cron: 30 17 * * *
jobs:
update-svg:
name: Update contributors SVG
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 7
- name: Set node version to 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
cache: "pnpm"
- name: Install deps
run: pnpm install
- name: Run generation script
run: pnpm tsx src/main.ts -t ${{ secrets.TOKEN }} -o continew-org
- name: Copy
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USERNAME }}
password: ${{ secrets.SERVER_PASSWORD }}
source: ./.github-contributors/*
target: ${{ secrets.SERVER_PATH }}
strip_components: 1
- name: Update image
run: |
git config user.email "charles7c@126.com"
git config user.name "Charles7c"
git add .
git commit -m "workflow: update image" && git push origin main || exit 0

View File

@@ -0,0 +1,50 @@
name: update-continew-starter-contributors-svg
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
# Schedule the interval of the checks.
schedule:
- cron: 00 17 * * *
jobs:
update-svg:
name: Update contributors SVG
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 7
- name: Set node version to 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
cache: "pnpm"
- name: Install deps
run: pnpm install
- name: Run generation script
run: pnpm tsx src/main.ts -t ${{ secrets.TOKEN }} -o continew-org -r continew-starter
- name: Update image
run: |
git config user.email "charles7c@126.com"
git config user.name "Charles7c"
git add .
git commit -m "workflow: update image" && git push origin main || exit 0

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.DS_Store
lib

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmjs.org

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
# github-contributor-svg-generator
## 简介
生成 GitHub 组织或仓库贡献者 SVG 图,基于 GitHub 的 `/repos/{owner}/{repo}/contributors` 接口获取贡献者数据,并生成贡献者 SVG 图。
本项目 Fork 自 [ShenQingchuan/github-contributor-svg-generator](https://github.com/ShenQingchuan/github-contributor-svg-generator)。
## 和原项目区别
原项目是根据仓库 Pull Request 及其相关用户的 Commit 数量制作贡献者 SVG 图,不会统计没有提交过 Pull Request 的用户。
本项目是基于 GitHub 的 [/repos/{owner}/{repo}/contributors](https://docs.github.com/zh/rest/repos/repos?apiVersion=2022-11-28#list-repository-contributors) 接口获取贡献者数据生成贡献者 SVG 图,并且支持生成组织贡献者 SVG 图(汇总组织下所有仓库贡献者前 100 名)。
## 使用方法
```bash
# 获取 GitHub Token
# 克隆项目
git clone https://github.com/charles7c/github-contributor-svg-generator.git
# 安装依赖
pnpm install
# 运行1生成 continew-org/continew-admin 仓库贡献者 SVG 图(替换下方 <GitHub Token> 为真实值)
pnpm tsx src/main.ts -t <GitHub Token> -o continew-org -r continew-admin
# 运行2生成 continew-org 贡献者 SVG 图(替换下方 <GitHub Token> 为真实值)
pnpm tsx src/main.ts -t <GitHub Token> -o continew-org
```
**提示:** 执行完成后会在你项目的根目录创建一个 `.github-contributors` 文件夹来存放 SVG 文件。

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "github-contributor-svg-generator",
"bin": {
"gh-contrib-svg": "./lib/main.js"
},
"main": "./lib/main.js",
"module": "./lib/main.js",
"files": [
"lib",
"README.md"
],
"version": "1.0.0",
"description": "Generate Contributors SVG for github repo.",
"type": "module",
"author": "Charles7c",
"license": "MIT",
"scripts": {
"build": "tsup",
"prepublish": "tsup"
},
"dependencies": {
"@octokit/core": "^4.0.4",
"commander": "^9.4.0",
"image-data-uri": "^2.0.1",
"node-fetch": "^3.2.9",
"ofetch": "^1.0.1",
"ora": "^6.1.2",
"sharp": "^0.31.3",
"tsx": "^3.8.0"
},
"devDependencies": {
"@types/node": "^18.6.2",
"bumpp": "^9.1.0",
"tsup": "^6.7.0",
"typescript": "^5.0.2"
}
}

2474
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

19
src/constants.ts Normal file
View File

@@ -0,0 +1,19 @@
export const SVG_DIST_DIR_NAME = '.github-contributors'
export const SVG_STYLESHEETS = `<style>
text {
font-weight: 300;
font-size: 12px;
fill: #777777;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
image {
border-radius: 50%;
}
.contributor-link {
cursor: pointer;
}
.contributors-title {
font-weight: 500;
font-size: 20px;
}
</style>`

237
src/fetch.ts Normal file
View File

@@ -0,0 +1,237 @@
import { Octokit } from "@octokit/core"
import ora from 'ora'
import type { ContributorsInfo, ContributorsInfoMap } from './types'
/**
* Traverse all pages for data collection, and break when breakOn returns true.
*
* @param requestByPage Request function by page.
* @param breakOn Break condition function.
* @returns Data collection.
*/
async function traversePagesForCount(
requestByPage: (page: number) => Promise<any>,
breakOn?: (resp: any) => boolean,
) {
let page = 1;
const dataCollection: any[] = [];
while (true) {
const resp = await requestByPage(page);
if (resp.data.length === 0) {
break;
}
if (breakOn?.(resp)) {
dataCollection.push(...resp.data)
break;
}
page++
dataCollection.push(...resp.data)
}
return dataCollection
}
/**
* Fetch repo create time.
*
* @param octokit Octokit instance.
* @param owner Owner of the repo.
* @param repo Repo name.
* @returns Repo create time.
* @throws Error when fetching repo create time failed.
* @example
* const octokit = new Octokit({ auth: 'YOUR_TOKEN' })
* const repoCreateTime = await getRepoCreateTime(octokit, 'octokit', 'rest.js')
* console.log(repoCreateTime) // 2021-01-01T00:00:00.000Z
*/
async function getRepoCreateTime(octokit: Octokit, owner: string, repo: string) {
try {
const repoData = await octokit.request(
'GET /repos/{owner}/{repo}',
{
owner,
repo,
}
)
return new Date(repoData.data.created_at)
} catch (e) {
console.error(`Fetch repo create time error: ${e}`)
throw e
}
}
/**
* Fetch repos info from GitHub API.
*
* @param params Fetch repos info params.
* @returns Repos info.
*/
export async function fetchRepos(params: {
token: string,
owner: string,
}) {
const { token, owner } = params
const octokit = new Octokit({ auth: token })
const reposData: any[] = []
const loadingSpin = ora(`Fetching ${owner} repos...`).start()
// fetch repos infos
try {
const reposRespData = await traversePagesForCount(
(page) => octokit.request(
'GET /orgs/{owner}/repos{?type,page,per_page}',
{
owner,
type: 'public',
page,
per_page: '100',
}
)
)
reposData.push(...reposRespData)
loadingSpin.succeed(`Fetching ${owner} repos done`)
// "full_name": "octocat/Hello-World"
return reposData.map(repo => ({ owner, repo: repo.full_name.split('/')[1] }))
} catch (err) {
console.log(`Error: Fetching ${owner} repos failed! ${err}`)
}
}
/**
* Fetch contributors info from GitHub API.
*
* @param params Fetch contributors info params.
* @returns Contributors info map.
*/
export async function fetchContributorsInfo(params: {
token: string,
owner: string,
repo: string,
}) {
const { token, owner, repo } = params
const octokit = new Octokit({ auth: token })
const contributorsData: any[] = []
const loadingSpin = ora(`Fetching ${owner}/${repo} contributors...`).start()
// fetch contributors infos
try {
const contributorsRespData = await traversePagesForCount(
(page) => octokit.request(
'GET /repos/{owner}/{repo}/contributors{?anon,page,per_page}',
{
owner,
repo,
anon: true,
page,
per_page: '100',
}
),
)
contributorsData.push(...contributorsRespData)
loadingSpin.succeed(`Fetching ${owner}/${repo} contributors done`)
} catch (err) {
console.log(`Error: Fetching ${owner}/${repo} contributors failed! ${err}`)
}
// create a map for all contributors infos
const allContributorsInfos = new Map<string, ContributorsInfo>()
contributorsData.forEach(contributor => {
const [userName, avatarURL] = [contributor.login, contributor.avatar_url]
const userInfoByName = allContributorsInfos.get(userName)
if (!userInfoByName) {
allContributorsInfos.set(userName, {
avatarURL,
commitURLs: [],
})
}
})
// count commits for all contributors we got in the map now
await supplementContributorsCommits({
token,
repo,
owner,
contributorsMap: allContributorsInfos
})
return allContributorsInfos
}
/**
* Supplement contributors commits info to contributors map.
*
* @param params Supplement contributors commits info params.
* @returns void.
*/
export async function supplementContributorsCommits(params: {
token: string,
owner: string,
repo: string,
contributorsMap: ContributorsInfoMap,
}) {
const { token, owner, repo, contributorsMap } = params
const octokit = new Octokit({ auth: token })
const commitsData: any[] = []
const repoCreateTime = await getRepoCreateTime(
octokit,
owner,
repo
);
const loadingSpin = ora(`Fetching ${owner}/${repo} commits...`).start()
try {
const commitsRespData = await traversePagesForCount(
(page) => octokit.request(
'GET /repos/{owner}/{repo}/commits{?page,per_page,since}',
{
owner,
repo,
page,
per_page: '100',
}
),
(resp) => {
const commits = resp.data
if (commits.some((commit: any) => {
const { commit: { author: { date } } } = commit
return new Date(date).getTime() < repoCreateTime.getTime()
})) {
return true
}
return false
}
)
commitsData.push(...commitsRespData
.filter((commit: any) => Boolean(commit?.author?.login))
.map((commit: any) => {
const { author: { login: userName }, commit: { author: { date }, url } } = commit
return { userName, url, date }
})
)
loadingSpin.succeed(`Fetching ${owner}/${repo} commits done`)
} catch (err) {
console.log(`Error: Fetching ${owner}/${repo} contributors commits failed! ${err}`)
}
loadingSpin.start('Supplementing commits info to contributors map...')
commitsData
.filter(
commit => new Date(commit.date).getTime() > repoCreateTime.getTime()
)
.forEach(commitInfo => {
const { userName, url } = commitInfo
const foundUserInfoByName = contributorsMap.get(userName)
if (!foundUserInfoByName) {
return
}
const userInfoByName = contributorsMap.get(userName)!
if (!userInfoByName.commitURLs) {
userInfoByName.commitURLs = []
}
userInfoByName.commitURLs.push(url)
})
loadingSpin.succeed('Supplementing commits done')
}

99
src/main.ts Normal file
View File

@@ -0,0 +1,99 @@
import { program } from 'commander'
import { fetchRepos, fetchContributorsInfo } from './fetch'
import { checkContribsPersistence, saveContribsPersistence } from './persistence'
import { saveSVG as saveSVG } from './save-svg'
import { generateContributorsSVGFile } from './svg-codegen'
import { getRepoName } from './utils'
import type { CliOptions, RepoInfo, ContributorsInfo } from './types'
async function main() {
const { Github_token: defaultToken, Github_owner: defaultOwner } = process.env
const defaultRepoName = await getRepoName()
const GITHUBReg = /https:\/\/github.com\/([\w\-_]+)\/([\w\-_]+)/
let urlInfo = null
program
.name('gh-contrib-svg')
.arguments('[url]')
.option('-t, --token <token>', 'Personal GitHub token', defaultToken)
.option('-o, --owner <owner>', 'Repo owner name', defaultOwner)
.option('-r, --repo <repo>', 'GitHub repo path', defaultRepoName)
.option('-s, --size <size>', 'Single avatar block size (pixel)', "120")
.option('-w, --width <width>', 'Output image width (pixel)', "1000")
.option('-c, --count <count>', 'Avatar count in one line', "8")
.action((url) => {
if (!url) return
const match = url.match(GITHUBReg)
if (!match)
throw new Error('Invalid GitHub Repo URL')
const [_, owner, repo] = match
urlInfo = {
owner,
repo
}
})
.parse(process.argv)
const options = Object.assign(program.opts(), urlInfo)
const { token, repo, owner, size: avatarBlockSize, width, count: lineCount } = options as CliOptions
if (token && owner) {
let repos: RepoInfo[] = []
let identifier = 'contributor_'
if (repo) {
// fetch <owner>/<repo> contributors info
identifier += repo;
repos.push({ owner, repo })
} else {
// fetch <owner> contributors info
identifier += owner;
const ownerRepos = await fetchRepos({ token, owner })
if (!ownerRepos || ownerRepos.length === 0) {
throw new Error('No repos found')
}
repos = [...repos, ...ownerRepos]
}
const startTime = performance.now()
const allContributorsInfos = new Map<string, ContributorsInfo>()
for (const { owner, repo } of repos) {
const contributorsInfos = await fetchContributorsInfo({ token, repo, owner });
contributorsInfos.forEach((info, username) => {
allContributorsInfos.set(username, info);
});
}
// sort contributors by commit count and pull request count
const sortedContributors = [...allContributorsInfos.entries()]
.sort(([, userInfoA], [, userInfoB]) => {
const countA = userInfoA.commitURLs.length
const countB = userInfoB.commitURLs.length
return countB - countA
})
const contribUserNames = sortedContributors.map(([userName,]) => userName);
checkContribsPersistence(
contribUserNames,
identifier
)
const svgString = await generateContributorsSVGFile({
imgWidth: Number(width),
blockSize: Number(avatarBlockSize),
lineCount: Number(lineCount),
}, new Map(sortedContributors))
saveSVG(svgString, identifier);
saveContribsPersistence(
contribUserNames,
identifier
)
const endTime = performance.now()
console.log(`Time cost: ${Math.round((endTime - startTime) / 1000)}s`)
} else {
if (!token)
throw new Error('Personal GitHub token is required')
if (!owner)
throw new Error('GitHub repo path is required')
}
}
main()

34
src/persistence.ts Normal file
View File

@@ -0,0 +1,34 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { SVG_DIST_DIR_NAME } from './constants'
const distDir = path.resolve(process.cwd(), SVG_DIST_DIR_NAME)
export function checkContribsPersistence(contribUserNames: string[], identifier: string) {
if (!existsSync(distDir)) {
mkdirSync(distDir)
}
const distDataFilePath = path.join(distDir, `${identifier}.json`)
if (!existsSync(distDataFilePath)) {
return
}
try {
const persistenceDataJsonStr = String(readFileSync(distDataFilePath))
if (JSON.stringify(contribUserNames) === persistenceDataJsonStr) {
console.log('\nThere are no new contributors.\n')
process.exit()
}
} catch (err) {
console.log(`\nError: check contribs persistence failed ! ${err}`)
process.exit()
}
}
export function saveContribsPersistence(contribUserNames: string[], identifier: string) {
const distDataFilePath = path.join(distDir, `${identifier}.json`)
writeFileSync(
distDataFilePath,
JSON.stringify(contribUserNames)
)
}

18
src/save-svg.ts Normal file
View File

@@ -0,0 +1,18 @@
import path from 'node:path';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { SVG_DIST_DIR_NAME } from './constants';
export function saveSVG(svgString: string, identifier: string) {
const distDir = path.join(process.cwd(), SVG_DIST_DIR_NAME)
if (!existsSync(distDir)) {
mkdirSync(distDir)
}
const optimizedSvgString = svgString.replace('\n', '').replace(/\s+/g, ' ')
const distFilePath = path.join(distDir, `${identifier}.svg`)
console.log(`Write SVG file to ${distFilePath}`)
writeFileSync(
distFilePath,
optimizedSvgString,
{ encoding: 'utf-8' }
)
}

150
src/svg-codegen.ts Normal file
View File

@@ -0,0 +1,150 @@
import ora from 'ora'
import { $fetch } from 'ofetch'
import sharp from 'sharp'
import { SVG_STYLESHEETS } from './constants'
import type { ContributorsInfo } from './types'
// @ts-expect-error missing types
import imageDataURI from 'image-data-uri'
function toBuffer(ab: ArrayBuffer) {
const buf = Buffer.alloc(ab.byteLength)
const view = new Uint8Array(ab)
for (let i = 0; i < buf.length; ++i)
buf[i] = view[i]
return buf
}
async function round(image: string | ArrayBuffer, radius = 0.5, size = 100) {
const rect = Buffer.from(
`<svg><rect x="0" y="0" width="${size}" height="${size}" rx="${size * radius}" ry="${size * radius}"/></svg>`,
)
return await sharp(typeof image === 'string' ? image : toBuffer(image))
.resize(size, size, { fit: sharp.fit.cover })
.composite([{
blend: 'dest-in',
input: rect,
density: 72,
}])
.png({ quality: 80, compressionLevel: 8 })
.toBuffer()
}
async function getAvatarDataURI(avatarURL: string) {
const avatarData = await $fetch(avatarURL, { responseType: 'arrayBuffer' })
const avatarDataURL = await imageDataURI.encode(
await round(avatarData, 0.5, 50), 'PNG'
)
return avatarDataURL
}
async function getImgSVGElement(params: {
imgX: number,
imgY: number,
imgSize: number,
avatarURL: string,
}) {
const { imgX, imgY, imgSize, avatarURL } = params
try {
return `<image x="${imgX}" y="${imgY}" width="${imgSize}" height="${imgSize}" xlink:href="${avatarURL}" clip-path="url(#avatarClipPath)" />`
} catch (e) {
console.error(`Fetch user avatar error: ${e}`)
throw e
}
}
const getContributorSVGTitle = (centerX: number, yStart: number) => {
return `<text class="contributors-title" x="${centerX}" y="${yStart}" text-anchor="middle">Contributors</text>`
}
const getNameTextSVGElement = (params: { textX: number; textY: number; text: string }) => {
const { textX, textY, text } = params
return `<text x="${textX}" y="${textY}" text-anchor="middle" class="contributor-name" fill="currentColor">${text}</text>`
}
const getAnchorWrapSVGElement = (userName: string, innerHTML: string) => {
const githubProfileURL = `https://github.com/${userName}`
return `<a class="contributor-link" xlink:href="${githubProfileURL}" target="_blank" id="${userName}">\n ${innerHTML}\n</a>\n`
}
const getSVGHeader = (imgWidth: number, imgHeight: number) => {
const clipPathDefs = '\n <defs><clipPath id="avatarClipPath" clipPathUnits="objectBoundingBox"><circle cx="0.5" cy=".5" r=".5"/></clipPath></defs>\n'
return `<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${imgWidth} ${imgHeight}" width="${imgWidth}" height="${imgHeight}"
>` + clipPathDefs
}
export async function generateContributorsSVGFile(
params: {
imgWidth: number,
blockSize: number,
lineCount: number,
},
contributorsMap: Map<string, ContributorsInfo>
) {
const { imgWidth, blockSize, lineCount } = params
if (lineCount % 2 !== 0) {
throw Error('[Generating SVG] line count must be even')
}
const generatingSvgSpin = ora('Generating SVG file...').start()
// Hint: These constants may be able to be customized by config params
const Y_START = 40
const TITLE_FONT_SIZE = 20
const TEXT_FONT_SIZE = 14
const Y_CONTENT_START = Y_START + TITLE_FONT_SIZE
const MARGIN = 18
const CENTER = imgWidth / 2
const AVATAR_SIZE = blockSize * 0.625
const SPACE = (blockSize - AVATAR_SIZE) / 2
const startX = CENTER - ((lineCount / 2) * blockSize) + SPACE
const getTextX = (imgX: number) => imgX + (AVATAR_SIZE / 2)
// let svgContent = `
// ${SVG_STYLESHEETS}
// ${getContributorSVGTitle(CENTER, Y_START)}
// `;
let svgContent = `
${SVG_STYLESHEETS}
`;
// Convert all contributors' avatar URL to DataURI
await Promise.all(
Array.from(contributorsMap.entries())
.map(async ([userName, contribInfo]) => {
const avatarDataURI: string = await getAvatarDataURI(contribInfo.avatarURL)
contributorsMap.get(userName)!.avatarURL = avatarDataURI
})
)
const contributorsIterator = contributorsMap.entries()
let contributorEntry = contributorsIterator.next()
let countForLine = 0
let lineIndex = 0
while (!contributorEntry.done) {
const [userName, contributorInfo] = contributorEntry.value
const imgX = startX + (countForLine * blockSize)
const imgY = Y_CONTENT_START + MARGIN + (lineIndex * (AVATAR_SIZE + TEXT_FONT_SIZE + MARGIN))
const imgSVGElement = await getImgSVGElement({
imgX, imgY,
imgSize: AVATAR_SIZE,
avatarURL: contributorInfo.avatarURL,
})
const textX = getTextX(imgX)
const textY = imgY + AVATAR_SIZE + MARGIN
const nameTextSVGElement = getNameTextSVGElement({
textX, textY, text: userName,
})
const anchorWrapSVGElement = getAnchorWrapSVGElement(userName, `${imgSVGElement}\n${nameTextSVGElement}`)
svgContent += anchorWrapSVGElement
countForLine += 1
if (countForLine === lineCount) {
countForLine = 0
lineIndex += 1
}
contributorEntry = contributorsIterator.next()
}
svgContent = `${getSVGHeader(imgWidth, ((lineIndex + 1) * blockSize) + MARGIN + Y_START)}\n${svgContent}\n</svg>`
generatingSvgSpin.succeed('Generated SVG content string.')
return svgContent
}

17
src/types.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface CliOptions {
token: string // Github access token
owner: string
repo: string
size: string
width: string
count: string
}
export interface ContributorsInfo {
avatarURL: string,
commitURLs: string[]
}
export type ContributorsInfoMap = Map<string, ContributorsInfo>
export interface RepoInfo {
owner: string,
repo: string
}

25
src/utils.ts Normal file
View File

@@ -0,0 +1,25 @@
import path from 'path'
import fsp from 'fs/promises'
export function getDefaultValue(name: string) {
return process.env[name]
}
export async function getRepoName() {
try {
const { repository, name } = JSON.parse(await fsp.readFile(path.resolve(process.cwd(), './package.json'), 'utf-8'))
if (!repository)
return name
const url = repository?.url ?? repository
// "git + git@github.com:xx/xx.git"
// "https://github.com/tj/commander.js.git"
const match = url.match(/github.com[:\/]?[\w\-_]+\/([\w\-_]+)/)
if (match) {
return match[1]
}
} catch (e) {
}
}

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"types": [
"node", /* Include NodeJS-specific types. */
], /* Type declaration files to be included in compilation. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"outDir": "./lib", /* Output directory for generated declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

11
tsup.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['./src/*'],
format: ['esm'],
target: 'node16',
outDir: "./lib",
clean: true,
dts: false,
splitting: false,
})