From c540ead09a9e5f05a9a2ba17a79d8edfd66dc872 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:43:17 +0800 Subject: [PATCH] ci: make `biome lint` happy --- .github/workflows/check.yml | 19 ++++++ .github/workflows/update-gist.yml | 2 + biome.json | 20 ++++-- bun.lockb | Bin 5523 -> 9009 bytes package.json | 3 +- src/github-api-client.ts | 10 +-- src/index.ts | 107 +++++++++++++++++------------- src/language-stats.ts | 12 ++-- src/linguist-analyzer.ts | 92 ++++++++++++++----------- src/types/commit.d.ts | 5 +- src/types/event.d.ts | 11 +-- tsconfig.json | 2 +- 12 files changed, 171 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..e877d68 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,19 @@ +name: Check + +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . \ No newline at end of file diff --git a/.github/workflows/update-gist.yml b/.github/workflows/update-gist.yml index 92cee55..b54cc02 100644 --- a/.github/workflows/update-gist.yml +++ b/.github/workflows/update-gist.yml @@ -1,6 +1,8 @@ name: "Update Gist" on: + push: + branches: [main] schedule: - cron: "0 0 * * *" workflow_dispatch: diff --git a/biome.json b/biome.json index 7624ca2..292de41 100644 --- a/biome.json +++ b/biome.json @@ -1,17 +1,17 @@ { + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, "formatter": { "enabled": true, "formatWithErrors": false, - "ignore": [ - "bun.lockb", - "node_modules" - ], + "ignore": ["**/bun.lockb", "**/node_modules"], "attributePosition": "auto", "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, - "lineEnding": "lf", - "semicolons": "always" + "lineEnding": "lf" }, "javascript": { "formatter": { @@ -22,5 +22,11 @@ "formatter": { "trailingCommas": "none" } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } } -} \ No newline at end of file +} diff --git a/bun.lockb b/bun.lockb index 8b8ed82056fa6b8a186ea152d403ce4235a4c965..e63a544a747ef578e49efb12076cfa216a045be0 100755 GIT binary patch delta 3196 zcmd5;X;4#F6n-yxVhBr!A{bDhs30f-f+=iYbE zB_sAt+A8o}zNX;IAFXqag7ewQWJh&=dvAIVJFpl7N(g90O7e(vOp81QcZn@BM`oWd(9B zNNbQ{kT?#UEZ-@Hq6P=SKpFgT2Dufa45Sw1K#=n}IT@rQya#~9i3g+1Z(Xb|E=nz3 zTH{eMYfI?riZ^c#Il4V@V0b}F&GB-zYS5gZuYL^FPdIV$Nu0{3Oq6oP<&os+XP>%; z)a@w0;-Ea)G403J7XgcJ4-pE;bXrZnMG9#ZeUUUn??Pk(6}^N+L;oEq6sQD!1>}k# zgq}rYLKU4)qM@%Nh0wc@X6O@$j8W0MNi;K>Y^6ogn-J?*a1~NUiQo~E8MG&h+yLjJ zdqu|dp{QBCB3l5N(JOKtkjP#UUyIivs{onWtIat;B6>v}`u0RdSi8<}ISR+clmmQTpW#+mI3a{%q@iPTW zA~$(kN!Kgl8~X-k*~Guyd7AXSJ3t-kHvD4c;5NuPO)>cSvZ+Q+Y|QiF4J&U&JT_)ZU~-|Cz<~;+~&E} zt{61KknSm5^jO(!R~zSuKaccLQu!XKY*Vu=~>nLTuj)XcL$+s!{rEZO^g zmu6aBRg`+ljK@)x%Aw`*EOlA)+3bOTjWyS;Hz*s`uoq8n#itoQGX%eIm@ zoUcl1;dpa^k>6xU&UoH<_{ZXVeeUV5otyUM%$YHlV&*-({r*9ZI~|MGpG~jbAv(9H zz`kW|RnX3cL+&rXu6(|G{KjLSFJF-mr_DX6wqB6W5o0EBxAKj7zinBb@5s(Acb?px zy<0IUWks>)n{5RdAGVBMsvj1SC=q;;w^x3vrK4@9rIg2OsT(Yt9re) z)l`Ar&FTm(&fyAJfdg4+GtQ~&m7$?S=v6fb+dGmf8^tutE-o8p7_)&Yr~&l_YYx^Q zEK=-OiRTBLi0pxc2N!O93^o*%p_L+~J8eV8+lG515fUOXPJo+^|60(vi8EU1MDlDs zBKac7moO2YmTYr}9?R|OqF37%3Fk(O=s5oXxl-;&`2m83_N;~1@d&>Uo{0ph3o;Wk zGAU{dd0;DZ4g(yf%t@E3zHrA0Te~Z8xk2^LdHsD&i~{GE#UVD{-fkw z!1*%&G3`4grTupoz!eI)0&sV799RrJt4p6Dk0mmJSV*kx20HWgKHoQHv&Vt)!2)`f z!cXq6kO$2rDm$63C+^J#47UJS6)fz0J!WWsc@X+d@g<+H*d~O(6!10Ogv6=AjTr@MEjQY6a;JDuQH;2(iu>b%7 delta 882 zcmb7CO=uHQ5Pokrp-D_)wzNq$L5l?gX~H(yni%O%R7zWfvAJJK`P?W=IX@@(kqNcSmq>btj3tY?}yCXY8<8E{mcs;*-! z7e~{DIEP3+F6Kn+<&3!zr_d+NBT9&2L>pqGj%9%{EBa=Uv2BQl5bcN?`u>;am8I#Q zKkTDY@_`51c?#Be5ATAYkmSpdLG6Mf>NHePKZT%}6n=5YiXFTgiei#qg9_@m5Ht}r zgZda0QBOm~v=`R7Nj70A#$pn^0Z#CI|J1=`=P`}hHx5K^4P$Vm84 z*64p{Exoo}GQoN22CTZqux7tL- z&^8rj##Muh7}z!KLHo%-87F!GIz64Xvc5Iyf_YCsyT$19)%#n8GvbK>3m=TOC^03% z8o}X}{b9^NeV*N&3y1Qj(-_AfhFKgAcx9f46taxz7=C1Jd2{%QMc{*RWJPh7;5M@6 zK$2y@F-v!Ob8mLNvJ+XVs;HQZSoLN~``6OW@G^w(q%lR0-xn^GemQ|fMkCA& zL$cgqOq+GMZRK14y+w@26g95wVa7a8HlMs1c-g*B;7!`EFt%)TTY?GSL0IvrzE*4} gerawz5&OW$oSPAxbduk7Q0s3krTni%7?BEp0Md}CX8-^I diff --git a/package.json b/package.json index 18e4581..d2fef28 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "start": "bun src/index.ts" }, "devDependencies": { + "@biomejs/biome": "1.8.3", "@octokit/types": "^13.5.0", "@types/bun": "latest", "typescript": "^5.0.0" @@ -13,4 +14,4 @@ "dependencies": { "@octokit/request": "^9.1.3" } -} \ No newline at end of file +} diff --git a/src/github-api-client.ts b/src/github-api-client.ts index 1a9799b..6204473 100644 --- a/src/github-api-client.ts +++ b/src/github-api-client.ts @@ -1,13 +1,13 @@ -import { request } from "@octokit/request"; +import { request } from '@octokit/request'; const { GH_TOKEN } = process.env; export const githubRequest = request.defaults({ - baseUrl: "https://api.github.com", + baseUrl: 'https://api.github.com', headers: { - "content-type": "application/json", - "user-agent": "octokit-request.js/9.1.3 bun/1.1.6", - accept: "application/vnd.github.v3+json", + 'content-type': 'application/json', + 'user-agent': 'octokit-request.js/9.1.3 bun/1.1.6', + accept: 'application/vnd.github.v3+json', authorization: `bearer ${GH_TOKEN}`, }, per_page: 100, diff --git a/src/index.ts b/src/index.ts index 6371611..8742cee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,38 +1,43 @@ -import { githubRequest } from "./github-api-client"; -import { createLanguageStats } from "./language-stats"; -import { runLinguist, type FileData } from "./linguist-analyzer"; -import type { GetCommitContentsResponse } from "./types/commit"; -import type { CommonEvent, PushEvent } from "./types/event"; +import { githubRequest } from './github-api-client'; +import { createLanguageStats } from './language-stats'; +import { type FileData, runLinguist } from './linguist-analyzer'; +import type { GetCommitContentsResponse } from './types/commit'; +import type { PushEvent } from './types/event'; const { GH_TOKEN, GIST_ID, USERNAME, DAYS } = process.env; const validateEnv = (): void => { - if (!GH_TOKEN) throw new Error("GH_TOKEN is not provided."); - if (!GIST_ID) throw new Error("GIST_ID is not provided."); - if (!USERNAME) throw new Error("USERNAME is not provided."); + if (!GH_TOKEN) throw new Error('GH_TOKEN is not provided.'); + if (!GIST_ID) throw new Error('GIST_ID is not provided.'); + if (!USERNAME) throw new Error('USERNAME is not provided.'); }; const fetchCommits = async ( - username: string, - fromDate: Date + username: string | null, + fromDate: Date, ): Promise => { + if (username === null) { + throw new Error('USERNAME is not provided.'); + } + const maxEvents = 300; const perPage = 100; const pages = Math.ceil(maxEvents / perPage); const commits: GetCommitContentsResponse[] = []; for (let page = 0; page < pages; page++) { - const events = await githubRequest("GET /users/{username}/events", { + const events = await githubRequest('GET /users/{username}/events', { username, per_page: perPage, page, - }) + }); const pushEvents = events.data.filter( - (event): event is PushEvent => event.type === "PushEvent" && event.actor.login === username + (event): event is PushEvent => + event.type === 'PushEvent' && event.actor.login === username, ); const recentPushEvents = pushEvents.filter( - ({ created_at }) => new Date(created_at) > fromDate + ({ created_at }) => new Date(created_at) > fromDate, ); console.log(`${recentPushEvents.length} events fetched.`); @@ -40,20 +45,22 @@ const fetchCommits = async ( recentPushEvents.flatMap(({ repo, payload }) => payload.commits .filter((commit) => commit.distinct === true) - .map((commit) => githubRequest("GET /repos/{owner}/{repo}/commits/{ref}", { - owner: repo.name.split("/")[0], - repo: repo.name.split("/")[1], - ref: commit.sha, - })) - ) + .map((commit) => + githubRequest('GET /repos/{owner}/{repo}/commits/{ref}', { + owner: repo.name.split('/')[0], + repo: repo.name.split('/')[1], + ref: commit.sha, + }), + ), + ), ); commits.push( ...newCommits - .filter((result) => result.status === "fulfilled") + .filter((result) => result.status === 'fulfilled') .map((result) => { - return result.value.data - }) + return result.value.data; + }), ); if (recentPushEvents.length < pushEvents.length) { @@ -65,18 +72,19 @@ const fetchCommits = async ( }; const processCommits = (commits: GetCommitContentsResponse[]): FileData[] => { - const result = commits .filter((commit) => commit.parents.length <= 1) .flatMap((commit) => - commit.files?.map(({ filename, additions, deletions, changes, status, patch }) => ({ - path: filename, - additions, - deletions, - changes, - status, - patch, - })) + commit.files?.map( + ({ filename, additions, deletions, changes, status, patch }) => ({ + path: filename, + additions, + deletions, + changes, + status, + patch, + }), + ), ) .filter((fileData) => fileData !== undefined); @@ -84,27 +92,27 @@ const processCommits = (commits: GetCommitContentsResponse[]): FileData[] => { }; const updateGist = async (gistId: string, content: string) => { - const gist = await githubRequest("GET /gists/{gist_id}", { + const gist = await githubRequest('GET /gists/{gist_id}', { gist_id: gistId, }); - const filename = Object.keys(gist.data.files!)[0]; - await githubRequest("PATCH /gists/{gist_id}", { + const filename = Object.keys(gist.data.files ?? {})[0]; + await githubRequest('PATCH /gists/{gist_id}', { gist_id: gistId, files: { [filename]: { - filename: `Bryan’s Recent Coding Languages`, + filename: 'Bryan’s Recent Coding Languages', content, }, }, }); - console.log(`Update succeeded.`); + console.log('Update succeeded.'); }; const main = async () => { try { validateEnv(); - const username = USERNAME!; + const username = USERNAME ?? null; const days = Math.max(1, Math.min(30, Number(DAYS || 14))); const fromDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); @@ -113,21 +121,28 @@ const main = async () => { const commits = await fetchCommits(username, fromDate); console.log(`${commits.length} commits fetched.`); - console.log(`\n`); + console.log('\n'); const files = processCommits(commits); const langs = await runLinguist(files); - console.log("\nLanguage statistics:"); - langs.forEach((lang) => - console.log(`${lang.name}: ${lang.count} files, ${lang.additions + lang.deletions} changes`) - ); + console.log('\nLanguage statistics:'); + for (const lang of langs) { + console.log( + `${lang.name}: ${lang.count} files, ${lang.additions + lang.deletions} changes`, + ); + } const content = createLanguageStats(langs); - console.log("\nGenerated content:"); + console.log('\nGenerated content:'); console.log(content); - console.log(`\n`); + console.log('\n'); + + if (GIST_ID) { + await updateGist(GIST_ID, content); + } else { + throw new Error('GIST_ID is not provided.'); + } - await updateGist(GIST_ID!, content); } catch (e) { console.error(e); process.exitCode = 1; diff --git a/src/language-stats.ts b/src/language-stats.ts index a750f3c..85b3eb7 100644 --- a/src/language-stats.ts +++ b/src/language-stats.ts @@ -1,4 +1,4 @@ -import type { ProcessedLanguageStats } from "./linguist-analyzer"; +import type { ProcessedLanguageStats } from './linguist-analyzer'; const UNITS = [ { symbol: 'E', value: 1e18 }, @@ -24,7 +24,7 @@ const formatNumber = (num: number): string => { }; const generateBarChart = (percent: number, size: number): string => { - const symbols = "░▏▎▍▌▋▊▉█"; + const symbols = '░▏▎▍▌▋▊▉█'; const fractionComplete = Math.floor((size * 8 * percent) / 100); const fullBars = Math.floor(fractionComplete / 8); @@ -40,7 +40,9 @@ const generateBarChart = (percent: number, size: number): string => { ); }; -export const createLanguageStats = (languages: ProcessedLanguageStats[]): string => { +export const createLanguageStats = ( + languages: ProcessedLanguageStats[], +): string => { return languages .map(({ name, percent, additions, deletions }) => { const truncatedName = truncateString(name, 10).padEnd(10); @@ -50,9 +52,9 @@ export const createLanguageStats = (languages: ProcessedLanguageStats[]): string const formatChanges = `${formattedAdditions} /${formattedDeletions}`; const bar = generateBarChart(percent, 18); - const percentageString = percent.toFixed(2).padStart(5) + '%'; + const percentageString = `${percent.toFixed(2).padStart(5)}%`; return `${truncatedName} ${formatChanges} ${bar} ${percentageString}`; }) .join('\n'); -}; \ No newline at end of file +}; diff --git a/src/linguist-analyzer.ts b/src/linguist-analyzer.ts index f8a5401..ece07e5 100644 --- a/src/linguist-analyzer.ts +++ b/src/linguist-analyzer.ts @@ -1,6 +1,6 @@ -import { exec } from 'child_process'; -import { promises as fs } from 'fs'; -import path from 'path'; +import { exec } from 'node:child_process'; +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; export interface FileData { additions?: number; @@ -33,25 +33,29 @@ const runCommand = (command: string): Promise => new Promise((resolve, reject) => { console.debug(`run > ${command}`); const child = exec(command); - let stdout = ""; - let stderr = ""; + let stdout = ''; + let stderr = ''; if (child.stdout) { - child.stdout.on("data", (data) => (stdout += data)); + child.stdout.on('data', (data) => { + stdout += data; + }); } if (child.stderr) { - child.stderr.on("data", (data) => (stderr += data)); + child.stderr.on('data', (data) => { + stderr += data; + }); } - child.on("close", (code) => { + child.on('close', (code) => { console.debug(`exited with code ${code}`); return code === 0 ? resolve(stdout) : reject(stderr); }); }); const createDummyText = (count: number): string => { - return "\n".repeat(count); + return '\n'.repeat(count); }; const createFileContent = (fileData: FileData): string => { @@ -65,26 +69,32 @@ const createFileContent = (fileData: FileData): string => { return fileData.changes ? createDummyText(fileData.changes) : ''; }; -export const runLinguist = async (files: FileData[]): Promise => { +export const runLinguist = async ( + files: FileData[], +): Promise => { try { - await runCommand("git checkout --orphan temp && git rm -rf . && rm -rf *"); + await runCommand('git checkout --orphan temp && git rm -rf . && rm -rf *'); const processFileData = files.map((file, index) => ({ ...file, path: `${index}${path.extname(file.path)}`, })); - const pathFileMap = processFileData.reduce>((acc, file) => { - acc[file.path] = file; - return acc; - }, {}); + const pathFileMap = processFileData.reduce>( + (acc, file) => { + acc[file.path] = file; + return acc; + }, + {}, + ); await Promise.all([ ...processFileData.map((file) => - fs.writeFile(file.path, createFileContent(file)) + writeFile(file.path, createFileContent(file)), ), - runCommand(`echo "*.* linguist-detectable" > .gitattributes`), - runCommand(`git config user.name "dummy" && git config user.email "dummy@github.com"`), + runCommand('echo "*.* linguist-detectable" > .gitattributes'), + runCommand('git config user.name "dummy"'), + runCommand('git config user.email "dummy@github.com"'), ]); // Add files to git @@ -92,39 +102,41 @@ export const runLinguist = async (files: FileData[]): Promise { - const additions = stats.files.reduce( - (sum, filePath) => sum + (pathFileMap[filePath]?.additions ?? 0), - 0 - ); - const deletions = stats.files.reduce( - (sum, filePath) => sum + (pathFileMap[filePath]?.deletions ?? 0), - 0 - ); - return { - name, - additions, - deletions, - count: stats.files.length, - // Calculate initial percent based on the returned data - percent: (additions + deletions) // placeholder for percent, will be recalculated - }; - }); + const languageStats = Object.entries(linguistResult).map( + ([name, stats]) => { + const additions = stats.files.reduce( + (sum, filePath) => sum + (pathFileMap[filePath]?.additions ?? 0), + 0, + ); + const deletions = stats.files.reduce( + (sum, filePath) => sum + (pathFileMap[filePath]?.deletions ?? 0), + 0, + ); + return { + name, + additions, + deletions, + count: stats.files.length, + // Calculate initial percent based on the returned data + percent: additions + deletions, // placeholder for percent, will be recalculated + }; + }, + ); // Calculate total additions and deletions across all languages const totalChanges = languageStats.reduce( (sum, lang) => sum + lang.additions + lang.deletions, - 0 + 0, ); // Update the percent based on the total changes - languageStats.forEach(lang => { + for (const lang of languageStats) { lang.percent = ((lang.additions + lang.deletions) / totalChanges) * 100; - }); + } // Sort the language stats by the total changes return languageStats.sort((a, b) => b.percent - a.percent); diff --git a/src/types/commit.d.ts b/src/types/commit.d.ts index b339a6d..86d3d84 100644 --- a/src/types/commit.d.ts +++ b/src/types/commit.d.ts @@ -1,3 +1,4 @@ -import type { Endpoints } from "@octokit/types"; +import type { Endpoints } from '@octokit/types'; -export type GetCommitContentsResponse = Endpoints["GET /repos/{owner}/{repo}/commits/{ref}"]["response"]["data"]; \ No newline at end of file +export type GetCommitContentsResponse = + Endpoints['GET /repos/{owner}/{repo}/commits/{ref}']['response']['data']; diff --git a/src/types/event.d.ts b/src/types/event.d.ts index e8ac5cc..d9d651c 100644 --- a/src/types/event.d.ts +++ b/src/types/event.d.ts @@ -1,7 +1,8 @@ -import type { Endpoints } from "@octokit/types"; +import type { Endpoints } from '@octokit/types'; -type ListUserEventsResponse = Endpoints["GET /users/{username}/events"]["response"]["data"]; -export type CommonEvent = ListUserEventsResponse[number]; +type ListUserEventsResponse = + Endpoints['GET /users/{username}/events']['response']['data']; +type CommonEvent = ListUserEventsResponse[number]; interface PushEventCommit { sha: string; @@ -25,7 +26,7 @@ interface PushEventPayload { } export interface PushEvent extends CommonEvent { - type: "PushEvent"; + type: 'PushEvent'; created_at: string; - payload: PushEventPayload & CommonEvent["payload"]; + payload: PushEventPayload & CommonEvent['payload']; } diff --git a/tsconfig.json b/tsconfig.json index bdde3ba..2f343dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,6 @@ // Best practices "strict": true, "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, + "noFallthroughCasesInSwitch": true } }