From 35a530d70a72517dc281dc87baef6b7f0325895c Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 10:38:53 +0330 Subject: [PATCH 01/10] init - WIP --- package.json | 3 + packages/app/app/pages/stats/chart.vue | 165 +++++++++++++++++++ packages/app/package.json | 4 + packages/app/server/api/chart.get.ts | 143 ++++++++++++++++ pnpm-lock.yaml | 219 +++++++++++++++++++++++-- scripts/generate-quickchart-urls.js | 118 +++++++++++++ 6 files changed, 639 insertions(+), 13 deletions(-) create mode 100644 packages/app/app/pages/stats/chart.vue create mode 100644 packages/app/server/api/chart.get.ts create mode 100644 scripts/generate-quickchart-urls.js diff --git a/package.json b/package.json index 5ad70106..fee198e1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "license": "MIT", "devDependencies": { "@jsdevtools/ez-spawn": "^3.0.4", + "@types/adm-zip": "^0.5.7", "@types/node": "^20.14.2", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", @@ -52,6 +53,8 @@ }, "dependencies": { "@cloudflare/vitest-pool-workers": "^0.6.8", + "adm-zip": "^0.5.16", + "chartjs-adapter-date-fns": "^3.0.0", "wrangler": "^3.57.1" }, "packageManager": "pnpm@9.1.3+sha256.7f63001edc077f1cff96cacba901f350796287a2800dfa83fe898f94183e4f5f" diff --git a/packages/app/app/pages/stats/chart.vue b/packages/app/app/pages/stats/chart.vue new file mode 100644 index 00000000..d5fb7ce8 --- /dev/null +++ b/packages/app/app/pages/stats/chart.vue @@ -0,0 +1,165 @@ + + + + + + \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index 4cb43edb..e5a1a14a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,10 +21,13 @@ "@nuxtjs/mdc": "0.17.0", "@octokit/app": "^15.1.1", "@octokit/graphql": "^8.1.1", + "@octokit/openapi-types": "^25.1.0", "@octokit/plugin-paginate-rest": "^11.3.6", + "@octokit/rest": "^22.0.0", "@octokit/webhooks": "^13.4.1", "@vueuse/core": "^12.2.0", "@vueuse/nuxt": "^12.2.0", + "chart.js": "^4.5.0", "marked": "^15.0.4", "nuxt": "^3.15.0", "nuxt-shiki": "0.3.0", @@ -33,6 +36,7 @@ "string-similarity": "^4.0.4", "unstorage": "^1.16.0", "vue": "^3.5.13", + "vue-chartjs": "^5.3.2", "vue-router": "^4.4.3", "zod": "^3.23.8" }, diff --git a/packages/app/server/api/chart.get.ts b/packages/app/server/api/chart.get.ts new file mode 100644 index 00000000..b7f0883e --- /dev/null +++ b/packages/app/server/api/chart.get.ts @@ -0,0 +1,143 @@ +import { defineEventHandler, H3Event } from 'h3' +import { useOctokitInstallation } from '../utils/octokit' +import AdmZip from 'adm-zip' + +function extractCountsBlock(content: string): any | null { + const lines = content.split('\n') + let startIdx = -1 + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes('Counts: {')) { + startIdx = i + break + } + } + if (startIdx === -1) return null + // Collect lines until closing } + const blockLines = [] + for (let i = startIdx; i < lines.length; i++) { + blockLines.push(lines[i]) + if (lines[i].trim().endsWith('}')) break + } + // Join and clean up + let block = blockLines.join('\n') + // Remove the leading 'Counts: ' and timestamps + block = block.replace(/.*Counts: /, '') + block = block.replace(/^[^\{]*\{/, '{') // Remove anything before first { + // Remove timestamps at the start of each line + block = block.replace(/^[0-9TZ\-:\.]+Z /gm, '') + // Convert JS object to JSON (add quotes) + block = block.replace(/([a-zA-Z0-9_]+):/g, '"$1":') + console.log('Counts block before JSON parse:', block) + try { + return JSON.parse(block) + } catch (e) { + console.error('Failed to parse Counts block as JSON:', block, e) + return null + } +} + +let cachedData: any = null; +let lastFetch = 0; +const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week in ms + +export default defineEventHandler(async (event: H3Event) => { + if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { + console.log('Serving /api/chart from cache') + return cachedData; + } + const owner = 'stackblitz-labs' + const repo = 'pkg.pr.new' + const workflowId = 'stats.yml' + + const octokit = await useOctokitInstallation(event, owner, repo) + + const runs = await octokit.paginate( + octokit.rest.actions.listWorkflowRuns, + { + owner, + repo, + workflow_id: workflowId, + per_page: 100, + status: 'success', + } + ) + + // Only process the latest 100 runs for performance + const latestRuns = runs.slice(0, 100) + + // 4. For each run, download logs, unzip, and extract Counts + const results = [] + for (const run of latestRuns) { + let stats = null + try { + console.log(`Requesting logs for run #${run.run_number} (id: ${run.id})`) + const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs({ + owner, + repo, + run_id: run.id + }) + console.log(`logsResponse.status for run #${run.run_number}:`, logsResponse.status) + console.log(`logsResponse.headers for run #${run.run_number}:`, logsResponse.headers) + if (logsResponse.status === 302) { + console.log(`302 redirect location for run #${run.run_number}:`, logsResponse.headers['location'] || logsResponse.headers['Location']) + } + if (!logsResponse.data) { + console.log(`No data received for run #${run.run_number} (id: ${run.id})`) + } else { + console.log('logsResponse.data instanceof Buffer:', Buffer.isBuffer(logsResponse.data)) + console.log('logsResponse.data constructor:', logsResponse.data?.constructor?.name) + console.log('typeof logsResponse.data:', typeof logsResponse.data) + let buffer + if (Buffer.isBuffer(logsResponse.data)) { + buffer = logsResponse.data + } else if (logsResponse.data instanceof ArrayBuffer) { + buffer = Buffer.from(new Uint8Array(logsResponse.data)) + } else { + throw new Error('logsResponse.data is not a Buffer or ArrayBuffer') + } + console.log(`Buffer size for run #${run.run_number} (id: ${run.id}): ${buffer.length} bytes`) + const zip = new AdmZip(buffer) + const entries = zip.getEntries() + console.log(`Run #${run.run_number} (id: ${run.id}) log files:`, entries.map(e => e.entryName)) + console.log(`Run #${run.run_number} (id: ${run.id}) zip entry count: ${entries.length}`) + if (entries.length === 0) { + console.log(`No log entries found in zip for run #${run.run_number} (id: ${run.id})`) + } + let found = false + for (const entry of entries) { + const content = entry.getData().toString('utf8') + const extracted = extractCountsBlock(content) + if (extracted) { + console.log(`Found Counts block in ${entry.entryName} for run #${run.run_number} (id: ${run.id}):`, extracted) + stats = extracted + found = true + break + } + } + if (!found) { + console.log(`No Counts block found in any log file for run #${run.run_number} (id: ${run.id})`) + } + } + } catch (e) { + if (e?.message && e.message.includes('Server Error')) { + console.warn(`Logs missing for run #${run.run_number} (id: ${run.id}) - likely due to retention policy. Skipping.`) + } else { + console.error(`Failed to fetch, unzip, or parse logs for run #${run.run_number} (id: ${run.id}):`, e) + } + } + if (stats) { + results.push({ + run_number: run.run_number, + created_at: run.created_at, + stats + }) + } + } + + console.log('Extracted stats:', JSON.stringify(results, null, 2)) + + // 5. Return JSON + cachedData = { runs: results }; + lastFetch = Date.now(); + return cachedData; +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d63b37b7..e6933052 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,12 @@ importers: '@cloudflare/vitest-pool-workers': specifier: ^0.6.8 version: 0.6.16(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.0)(date-fns@4.1.0) wrangler: specifier: ^3.57.1 version: 3.99.0 @@ -23,6 +29,9 @@ importers: '@jsdevtools/ez-spawn': specifier: ^3.0.4 version: 3.0.4 + '@types/adm-zip': + specifier: ^0.5.7 + version: 0.5.7 '@types/node': specifier: ^20.14.2 version: 20.17.10 @@ -101,9 +110,15 @@ importers: '@octokit/graphql': specifier: ^8.1.1 version: 8.1.1 + '@octokit/openapi-types': + specifier: ^25.1.0 + version: 25.1.0 '@octokit/plugin-paginate-rest': specifier: ^11.3.6 version: 11.3.6(@octokit/core@6.1.4) + '@octokit/rest': + specifier: ^22.0.0 + version: 22.0.0 '@octokit/webhooks': specifier: ^13.4.1 version: 13.4.1 @@ -113,6 +128,9 @@ importers: '@vueuse/nuxt': specifier: ^12.2.0 version: 12.5.0(magicast@0.3.5)(nuxt@3.15.3(@parcel/watcher@2.5.0)(@types/node@20.17.10)(db0@0.2.1)(eslint@8.57.1)(ioredis@5.4.2)(lightningcss@1.28.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.2)(vite@6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1))(vue-tsc@2.1.10(typescript@5.7.2))(yaml@2.7.1))(typescript@5.7.2) + chart.js: + specifier: ^4.5.0 + version: 4.5.0 marked: specifier: ^15.0.4 version: 15.0.4 @@ -137,6 +155,9 @@ importers: vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.2) + vue-chartjs: + specifier: ^5.3.2 + version: 5.3.2(chart.js@4.5.0)(vue@3.5.13(typescript@5.7.2)) vue-router: specifier: ^4.4.3 version: 4.5.0(vue@3.5.13(typescript@5.7.2)) @@ -1676,6 +1697,9 @@ packages: '@jspm/core@2.1.0': resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -1881,6 +1905,10 @@ packages: resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} engines: {node: '>= 18'} + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + '@octokit/auth-unauthenticated@5.0.1': resolution: {integrity: sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==} engines: {node: '>= 18'} @@ -1905,6 +1933,10 @@ packages: resolution: {integrity: sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==} engines: {node: '>= 18'} + '@octokit/core@7.0.3': + resolution: {integrity: sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==} + engines: {node: '>= 20'} + '@octokit/endpoint@10.1.2': resolution: {integrity: sha512-XybpFv9Ms4hX5OCHMZqyODYqGTZ3H6K6Vva+M9LR7ib/xr1y1ZnlChYv9H680y77Vd/i/k+thXApeRASBQkzhA==} engines: {node: '>= 18'} @@ -1913,6 +1945,10 @@ packages: resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} engines: {node: '>= 18'} + '@octokit/endpoint@11.0.0': + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} + engines: {node: '>= 20'} + '@octokit/endpoint@9.0.5': resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==} engines: {node: '>= 18'} @@ -1929,6 +1965,10 @@ packages: resolution: {integrity: sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==} engines: {node: '>= 18'} + '@octokit/graphql@9.0.1': + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} + engines: {node: '>= 20'} + '@octokit/oauth-app@7.1.3': resolution: {integrity: sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==} engines: {node: '>= 18'} @@ -1958,6 +1998,9 @@ packages: '@octokit/openapi-types@24.2.0': resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + '@octokit/openapi-webhooks-types@10.4.0': resolution: {integrity: sha512-HMiF7FUiVBYfp8pPijMTkWuPELQB6XkPftrnSuK1C1YXaaq2+0ganiQkorEQfXTmhtwlgHJwXT6P8miVhIyjQA==} @@ -1982,12 +2025,24 @@ packages: peerDependencies: '@octokit/core': '>=6' + '@octokit/plugin-paginate-rest@13.1.1': + resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-paginate-rest@9.2.1': resolution: {integrity: sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '5' + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-rest-endpoint-methods@10.4.1': resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==} engines: {node: '>= 18'} @@ -2000,6 +2055,12 @@ packages: peerDependencies: '@octokit/core': '>=6' + '@octokit/plugin-rest-endpoint-methods@16.0.0': + resolution: {integrity: sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-retry@7.2.0': resolution: {integrity: sha512-psMbEYb/Fh+V+ZaFo8J16QiFz4sVTv3GntCSU+hYqzHiMdc3P+hhHLVv+dJt0PGIPAGoIA5u+J2DCJdK6lEPsQ==} engines: {node: '>= 18'} @@ -2024,6 +2085,14 @@ packages: resolution: {integrity: sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==} engines: {node: '>= 18'} + '@octokit/request-error@7.0.0': + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.3': + resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==} + engines: {node: '>= 20'} + '@octokit/request@8.4.0': resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==} engines: {node: '>= 18'} @@ -2036,6 +2105,10 @@ packages: resolution: {integrity: sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==} engines: {node: '>= 18'} + '@octokit/rest@22.0.0': + resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==} + engines: {node: '>= 20'} + '@octokit/types@12.6.0': resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} @@ -2045,6 +2118,9 @@ packages: '@octokit/types@13.6.2': resolution: {integrity: sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==} + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@octokit/webhooks-methods@5.1.0': resolution: {integrity: sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==} engines: {node: '>= 18'} @@ -2636,6 +2712,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/adm-zip@0.5.7': + resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} + '@types/aws-lambda@8.10.146': resolution: {integrity: sha512-3BaDXYTh0e6UCJYL/jwV/3+GRslSc08toAiZSmleYtkAUyV5rtvdPYxrG/88uqvTuT6sb27WE9OS90ZNTIuQ0g==} @@ -3145,6 +3224,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -3324,6 +3407,9 @@ packages: before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -3491,6 +3577,16 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + + chartjs-adapter-date-fns@3.0.0: + resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} + peerDependencies: + chart.js: '>=2.8.0' + date-fns: '>=2.0.0' + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -4521,6 +4617,9 @@ packages: fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -7838,6 +7937,12 @@ packages: vue-bundle-renderer@2.1.1: resolution: {integrity: sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==} + vue-chartjs@5.3.2: + resolution: {integrity: sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==} + peerDependencies: + chart.js: ^4.1.1 + vue: ^3.0.0-0 || ^2.7.0 + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -9230,6 +9335,8 @@ snapshots: '@jspm/core@2.1.0': {} + '@kurkle/color@0.3.4': {} + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.0(supports-color@9.4.0) @@ -9932,6 +10039,8 @@ snapshots: '@octokit/auth-token@5.1.1': {} + '@octokit/auth-token@6.0.0': {} + '@octokit/auth-unauthenticated@5.0.1': dependencies: '@octokit/request-error': 5.1.0 @@ -9977,6 +10086,16 @@ snapshots: before-after-hook: 3.0.2 universal-user-agent: 7.0.2 + '@octokit/core@7.0.3': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.1 + '@octokit/request': 10.0.3 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.2 + '@octokit/endpoint@10.1.2': dependencies: '@octokit/types': 13.6.2 @@ -9987,6 +10106,11 @@ snapshots: '@octokit/types': 13.10.0 universal-user-agent: 7.0.2 + '@octokit/endpoint@11.0.0': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.2 + '@octokit/endpoint@9.0.5': dependencies: '@octokit/types': 13.6.2 @@ -10010,6 +10134,12 @@ snapshots: '@octokit/types': 13.10.0 universal-user-agent: 7.0.2 + '@octokit/graphql@9.0.1': + dependencies: + '@octokit/request': 10.0.3 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.2 + '@octokit/oauth-app@7.1.3': dependencies: '@octokit/auth-oauth-app': 8.1.1 @@ -10054,6 +10184,8 @@ snapshots: '@octokit/openapi-types@24.2.0': {} + '@octokit/openapi-types@25.1.0': {} + '@octokit/openapi-webhooks-types@10.4.0': {} '@octokit/openapi-webhooks-types@8.5.1': {} @@ -10077,11 +10209,20 @@ snapshots: '@octokit/core': 6.1.4 '@octokit/types': 13.10.0 + '@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/types': 14.1.0 + '@octokit/plugin-paginate-rest@9.2.1(@octokit/core@5.2.0)': dependencies: '@octokit/core': 5.2.0 '@octokit/types': 12.6.0 + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.0)': dependencies: '@octokit/core': 5.2.0 @@ -10092,6 +10233,11 @@ snapshots: '@octokit/core': 6.1.4 '@octokit/types': 13.10.0 + '@octokit/plugin-rest-endpoint-methods@16.0.0(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/types': 14.1.0 + '@octokit/plugin-retry@7.2.0(@octokit/core@6.1.4)': dependencies: '@octokit/core': 6.1.4 @@ -10119,6 +10265,18 @@ snapshots: dependencies: '@octokit/types': 13.10.0 + '@octokit/request-error@7.0.0': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@10.0.3': + dependencies: + '@octokit/endpoint': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + fast-content-type-parse: 3.0.0 + universal-user-agent: 7.0.2 + '@octokit/request@8.4.0': dependencies: '@octokit/endpoint': 9.0.5 @@ -10141,6 +10299,13 @@ snapshots: fast-content-type-parse: 2.0.1 universal-user-agent: 7.0.2 + '@octokit/rest@22.0.0': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/plugin-paginate-rest': 13.1.1(@octokit/core@7.0.3) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.3) + '@octokit/plugin-rest-endpoint-methods': 16.0.0(@octokit/core@7.0.3) + '@octokit/types@12.6.0': dependencies: '@octokit/openapi-types': 20.0.0 @@ -10153,6 +10318,10 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@octokit/webhooks-methods@5.1.0': {} '@octokit/webhooks-methods@5.1.1': {} @@ -10695,6 +10864,10 @@ snapshots: '@trysound/sax@0.2.0': {} + '@types/adm-zip@0.5.7': + dependencies: + '@types/node': 20.17.10 + '@types/aws-lambda@8.10.146': {} '@types/color-convert@2.0.4': @@ -11373,6 +11546,8 @@ snapshots: acorn@8.14.1: {} + adm-zip@0.5.16: {} + agent-base@7.1.3: {} ajv-formats@2.1.1(ajv@8.17.1): @@ -11568,6 +11743,8 @@ snapshots: before-after-hook@3.0.2: {} + before-after-hook@4.0.0: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -11766,6 +11943,15 @@ snapshots: character-reference-invalid@2.0.1: {} + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + + chartjs-adapter-date-fns@3.0.0(chart.js@4.5.0)(date-fns@4.1.0): + dependencies: + chart.js: 4.5.0 + date-fns: 4.1.0 + check-error@2.1.1: {} chokidar@3.6.0: @@ -12520,10 +12706,10 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -12533,9 +12719,9 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-config-prettier: 8.10.0(eslint@8.57.1) - eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-node: 11.1.0(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -12562,7 +12748,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@9.4.0) @@ -12574,7 +12760,7 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import-x: 4.6.1(eslint@8.57.1)(typescript@5.7.2) transitivePeerDependencies: - supports-color @@ -12589,14 +12775,14 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -12643,7 +12829,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -12654,7 +12840,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13053,10 +13239,12 @@ snapshots: enhanced-resolve: 5.18.0 mlly: 1.7.4 pathe: 1.1.2 - ufo: 1.5.4 + ufo: 1.6.1 fast-content-type-parse@2.0.1: {} + fast-content-type-parse@3.0.0: {} + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -13418,7 +13606,7 @@ snapshots: iron-webcrypto: 1.2.1 ohash: 1.1.4 radix3: 1.1.2(patch_hash=gsdwrxpd7rn5ueaa55ufykpidm) - ufo: 1.5.4 + ufo: 1.6.1 uncrypto: 0.1.3 unenv: 1.10.0 @@ -17167,6 +17355,11 @@ snapshots: dependencies: ufo: 1.5.4 + vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.13(typescript@5.7.2)): + dependencies: + chart.js: 4.5.0 + vue: 3.5.13(typescript@5.7.2) + vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)): dependencies: vue: 3.5.13(typescript@5.7.2) diff --git a/scripts/generate-quickchart-urls.js b/scripts/generate-quickchart-urls.js new file mode 100644 index 00000000..8f7b120c --- /dev/null +++ b/scripts/generate-quickchart-urls.js @@ -0,0 +1,118 @@ +import fetch from 'node-fetch'; + +function formatMonth(dateString) { + const date = new Date(dateString); + return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }); +} + +async function main() { + const res = await fetch('http://localhost:3000/api/chart'); + const { runs } = await res.json(); + + // Sort by date + runs.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at)); + + // Format labels as 'May 2025', 'Jun 2025', etc. + const labels = runs.map(r => formatMonth(r.created_at)); + const orgs = runs.map(r => r.stats.orgs); + const repos = runs.map(r => r.stats.repos); + const prsAndBranches = runs.map(r => r.stats.prsAndBranches); + const commits = runs.map(r => r.stats.commits); + + // Orgs & Repos + const orgsReposConfig = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'Organizations', + data: orgs, + borderColor: 'rgb(54, 162, 235)', + fill: false, + }, + { + label: 'Repositories', + data: repos, + borderColor: 'rgb(255, 99, 132)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { beginAtZero: true, title: { display: true, text: 'Count' } } + } + } + }; + + // PRs & Branches + const prsBranchesConfig = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'PRs & Branches', + data: prsAndBranches, + borderColor: 'rgb(255, 159, 64)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { beginAtZero: true, title: { display: true, text: 'Count' } } + } + } + }; + + // Commits + const commitsConfig = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'Commits', + data: commits, + borderColor: 'rgb(153, 102, 255)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { beginAtZero: true, title: { display: true, text: 'Count' } } + } + } + }; + + function quickChartUrl(config) { + return 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)); + } + + console.log('\nOrgs & Repos Chart:\n'); + console.log(`![Orgs & Repos](${quickChartUrl(orgsReposConfig)})\n`); + console.log('\nPRs & Branches Chart:\n'); + console.log(`![PRs & Branches](${quickChartUrl(prsBranchesConfig)})\n`); + console.log('\nCommits Chart:\n'); + console.log(`![Commits](${quickChartUrl(commitsConfig)})\n`); +} + +main(); \ No newline at end of file From 10a1ed22a7e99bd4477743f3211dbe2065abf7a0 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 13:22:16 +0330 Subject: [PATCH 02/10] rm logs and use logarithmic scale --- packages/app/app/pages/stats/chart.vue | 12 +++++++----- packages/app/server/api/chart.get.ts | 25 +++---------------------- scripts/generate-quickchart-urls.js | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/packages/app/app/pages/stats/chart.vue b/packages/app/app/pages/stats/chart.vue index d5fb7ce8..1b2b4974 100644 --- a/packages/app/app/pages/stats/chart.vue +++ b/packages/app/app/pages/stats/chart.vue @@ -22,7 +22,8 @@ import { Title, Tooltip, Legend, - TimeScale + TimeScale, + LogarithmicScale } from 'chart.js' import 'chartjs-adapter-date-fns' @@ -35,7 +36,8 @@ ChartJS.register( Title, Tooltip, Legend, - TimeScale + TimeScale, + LogarithmicScale ) const orgsReposRef = ref(null) @@ -85,7 +87,7 @@ onMounted(async () => { title: { display: true, text: 'Date' } }, y: { - beginAtZero: true, + type: 'logarithmic', title: { display: true, text: 'Count' } } } @@ -117,7 +119,7 @@ onMounted(async () => { title: { display: true, text: 'Date' } }, y: { - beginAtZero: true, + type: 'logarithmic', title: { display: true, text: 'Count' } } } @@ -149,7 +151,7 @@ onMounted(async () => { title: { display: true, text: 'Date' } }, y: { - beginAtZero: true, + type: 'logarithmic', title: { display: true, text: 'Count' } } } diff --git a/packages/app/server/api/chart.get.ts b/packages/app/server/api/chart.get.ts index b7f0883e..2c5e4e14 100644 --- a/packages/app/server/api/chart.get.ts +++ b/packages/app/server/api/chart.get.ts @@ -27,7 +27,6 @@ function extractCountsBlock(content: string): any | null { block = block.replace(/^[0-9TZ\-:\.]+Z /gm, '') // Convert JS object to JSON (add quotes) block = block.replace(/([a-zA-Z0-9_]+):/g, '"$1":') - console.log('Counts block before JSON parse:', block) try { return JSON.parse(block) } catch (e) { @@ -42,7 +41,6 @@ const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week in ms export default defineEventHandler(async (event: H3Event) => { if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { - console.log('Serving /api/chart from cache') return cachedData; } const owner = 'stackblitz-labs' @@ -70,23 +68,15 @@ export default defineEventHandler(async (event: H3Event) => { for (const run of latestRuns) { let stats = null try { - console.log(`Requesting logs for run #${run.run_number} (id: ${run.id})`) const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs({ owner, repo, run_id: run.id }) - console.log(`logsResponse.status for run #${run.run_number}:`, logsResponse.status) - console.log(`logsResponse.headers for run #${run.run_number}:`, logsResponse.headers) if (logsResponse.status === 302) { - console.log(`302 redirect location for run #${run.run_number}:`, logsResponse.headers['location'] || logsResponse.headers['Location']) } if (!logsResponse.data) { - console.log(`No data received for run #${run.run_number} (id: ${run.id})`) } else { - console.log('logsResponse.data instanceof Buffer:', Buffer.isBuffer(logsResponse.data)) - console.log('logsResponse.data constructor:', logsResponse.data?.constructor?.name) - console.log('typeof logsResponse.data:', typeof logsResponse.data) let buffer if (Buffer.isBuffer(logsResponse.data)) { buffer = logsResponse.data @@ -95,34 +85,28 @@ export default defineEventHandler(async (event: H3Event) => { } else { throw new Error('logsResponse.data is not a Buffer or ArrayBuffer') } - console.log(`Buffer size for run #${run.run_number} (id: ${run.id}): ${buffer.length} bytes`) const zip = new AdmZip(buffer) const entries = zip.getEntries() - console.log(`Run #${run.run_number} (id: ${run.id}) log files:`, entries.map(e => e.entryName)) - console.log(`Run #${run.run_number} (id: ${run.id}) zip entry count: ${entries.length}`) if (entries.length === 0) { - console.log(`No log entries found in zip for run #${run.run_number} (id: ${run.id})`) } let found = false for (const entry of entries) { const content = entry.getData().toString('utf8') const extracted = extractCountsBlock(content) if (extracted) { - console.log(`Found Counts block in ${entry.entryName} for run #${run.run_number} (id: ${run.id}):`, extracted) stats = extracted found = true break } } if (!found) { - console.log(`No Counts block found in any log file for run #${run.run_number} (id: ${run.id})`) } } } catch (e) { - if (e?.message && e.message.includes('Server Error')) { - console.warn(`Logs missing for run #${run.run_number} (id: ${run.id}) - likely due to retention policy. Skipping.`) + if (e instanceof Error && e.message && e.message.includes('Server Error')) { + // skip } else { - console.error(`Failed to fetch, unzip, or parse logs for run #${run.run_number} (id: ${run.id}):`, e) + // console.error(e) } } if (stats) { @@ -133,9 +117,6 @@ export default defineEventHandler(async (event: H3Event) => { }) } } - - console.log('Extracted stats:', JSON.stringify(results, null, 2)) - // 5. Return JSON cachedData = { runs: results }; lastFetch = Date.now(); diff --git a/scripts/generate-quickchart-urls.js b/scripts/generate-quickchart-urls.js index 8f7b120c..6c11fd24 100644 --- a/scripts/generate-quickchart-urls.js +++ b/scripts/generate-quickchart-urls.js @@ -46,7 +46,10 @@ async function main() { type: 'category', title: { display: true, text: 'Month' } }, - y: { beginAtZero: true, title: { display: true, text: 'Count' } } + y: { + type: 'logarithmic', + title: { display: true, text: 'Count' } + } } } }; @@ -72,7 +75,10 @@ async function main() { type: 'category', title: { display: true, text: 'Month' } }, - y: { beginAtZero: true, title: { display: true, text: 'Count' } } + y: { + type: 'logarithmic', + title: { display: true, text: 'Count' } + } } } }; @@ -98,7 +104,10 @@ async function main() { type: 'category', title: { display: true, text: 'Month' } }, - y: { beginAtZero: true, title: { display: true, text: 'Count' } } + y: { + type: 'logarithmic', + title: { display: true, text: 'Count' } + } } } }; From 2c8e7503e3b08b6eabac77fcff62bbb483e4dc34 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 15:46:29 +0330 Subject: [PATCH 03/10] rm ui route - spearate api for imgs --- README.md | 7 + package.json | 1 - packages/app/app/pages/stats/chart.vue | 167 ------------------ .../app/server/api/chart-image-commits.get.ts | 61 +++++++ .../server/api/chart-image-orgs-repos.get.ts | 69 ++++++++ .../api/chart-image-prs-branches.get.ts | 61 +++++++ pnpm-lock.yaml | 48 ++--- scripts/generate-quickchart-urls.js | 127 ------------- 8 files changed, 217 insertions(+), 324 deletions(-) delete mode 100644 packages/app/app/pages/stats/chart.vue create mode 100644 packages/app/server/api/chart-image-commits.get.ts create mode 100644 packages/app/server/api/chart-image-orgs-repos.get.ts create mode 100644 packages/app/server/api/chart-image-prs-branches.get.ts delete mode 100644 scripts/generate-quickchart-urls.js diff --git a/README.md b/README.md index f061634b..e57d5f19 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ Show that your project uses pkg.pr.new by adding a badge to your README: [![pkg.pr.new](https://pkg.pr.new/badge/stackblitz-labs/pkg.pr.new)](https://pkg.pr.new/~/stackblitz-labs/pkg.pr.new) +

+ Growth Statistics
+ Orgs & Repos + PRs & Branches + Commits +

+ ### How to Get a Badge for Your Repository 1. **Automatically on Repository Page**: diff --git a/package.json b/package.json index fee198e1..6d158412 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "dependencies": { "@cloudflare/vitest-pool-workers": "^0.6.8", "adm-zip": "^0.5.16", - "chartjs-adapter-date-fns": "^3.0.0", "wrangler": "^3.57.1" }, "packageManager": "pnpm@9.1.3+sha256.7f63001edc077f1cff96cacba901f350796287a2800dfa83fe898f94183e4f5f" diff --git a/packages/app/app/pages/stats/chart.vue b/packages/app/app/pages/stats/chart.vue deleted file mode 100644 index 1b2b4974..00000000 --- a/packages/app/app/pages/stats/chart.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/app/server/api/chart-image-commits.get.ts b/packages/app/server/api/chart-image-commits.get.ts new file mode 100644 index 00000000..a64a1284 --- /dev/null +++ b/packages/app/server/api/chart-image-commits.get.ts @@ -0,0 +1,61 @@ +import { defineEventHandler } from 'h3' +import { $fetch } from 'ofetch' +import chartData from './chart.get' + +interface Run { + created_at: string; + stats: { commits: number } +} + +export default defineEventHandler(async (event) => { + const { runs } = await chartData(event) as { runs: Run[] } + runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at) + return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) + }) + const commits = runs.map((r: Run) => r.stats.commits) + function getMinMax(arr: number[]) { + const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) + const min = Math.min(...filtered) + const max = Math.max(...filtered) + return { min: min - 10, max: max + 10 } + } + const minMax = getMinMax(commits) + const config = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'Commits', + data: commits, + borderColor: 'rgb(153, 102, 255)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { + type: 'linear', + title: { display: true, text: 'Count' }, + min: minMax.min, + max: minMax.max, + ticks: { + stepSize: 10 + } + } + } + } + } + const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' + const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) + event.node.res.setHeader('Content-Type', 'image/png') + return Buffer.from(img) +}) diff --git a/packages/app/server/api/chart-image-orgs-repos.get.ts b/packages/app/server/api/chart-image-orgs-repos.get.ts new file mode 100644 index 00000000..b0dcefbf --- /dev/null +++ b/packages/app/server/api/chart-image-orgs-repos.get.ts @@ -0,0 +1,69 @@ +import { defineEventHandler } from 'h3' +import { $fetch } from 'ofetch' +import chartData from './chart.get' + +interface Run { + created_at: string; + stats: { orgs: number, repos: number } +} + +export default defineEventHandler(async (event) => { + const { runs } = await chartData(event) as { runs: Run[] } + runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at) + return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) + }) + const orgs = runs.map((r: Run) => r.stats.orgs) + const repos = runs.map((r: Run) => r.stats.repos) + function getMinMax(arr: number[]) { + const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) + const min = Math.min(...filtered) + const max = Math.max(...filtered) + return { min: min - 10, max: max + 10 } + } + const orgsMinMax = getMinMax(orgs) + const reposMinMax = getMinMax(repos) + const config = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'Organizations', + data: orgs, + borderColor: 'rgb(54, 162, 235)', + fill: false, + }, + { + label: 'Repositories', + data: repos, + borderColor: 'rgb(255, 99, 132)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { + type: 'linear', + title: { display: true, text: 'Count' }, + min: Math.min(orgsMinMax.min, reposMinMax.min), + max: Math.max(orgsMinMax.max, reposMinMax.max), + ticks: { + stepSize: 10 + } + } + } + } + } + const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' + const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) + event.node.res.setHeader('Content-Type', 'image/png') + return Buffer.from(img) +}) \ No newline at end of file diff --git a/packages/app/server/api/chart-image-prs-branches.get.ts b/packages/app/server/api/chart-image-prs-branches.get.ts new file mode 100644 index 00000000..1699204b --- /dev/null +++ b/packages/app/server/api/chart-image-prs-branches.get.ts @@ -0,0 +1,61 @@ +import { defineEventHandler } from 'h3' +import { $fetch } from 'ofetch' +import chartData from './chart.get' + +interface Run { + created_at: string; + stats: { prsAndBranches: number } +} + +export default defineEventHandler(async (event) => { + const { runs } = await chartData(event) as { runs: Run[] } + runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at) + return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) + }) + const prsAndBranches = runs.map((r: Run) => r.stats.prsAndBranches) + function getMinMax(arr: number[]) { + const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) + const min = Math.min(...filtered) + const max = Math.max(...filtered) + return { min: min - 10, max: max + 10 } + } + const minMax = getMinMax(prsAndBranches) + const config = { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'PRs & Branches', + data: prsAndBranches, + borderColor: 'rgb(255, 159, 64)', + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category', + title: { display: true, text: 'Month' } + }, + y: { + type: 'linear', + title: { display: true, text: 'Count' }, + min: minMax.min, + max: minMax.max, + ticks: { + stepSize: 10 + } + } + } + } + } + const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' + const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) + event.node.res.setHeader('Content-Type', 'image/png') + return Buffer.from(img) +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6933052..7d6a4881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,9 +19,6 @@ importers: adm-zip: specifier: ^0.5.16 version: 0.5.16 - chartjs-adapter-date-fns: - specifier: ^3.0.0 - version: 3.0.0(chart.js@4.5.0)(date-fns@4.1.0) wrangler: specifier: ^3.57.1 version: 3.99.0 @@ -3581,12 +3578,6 @@ packages: resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} engines: {pnpm: '>=8'} - chartjs-adapter-date-fns@3.0.0: - resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} - peerDependencies: - chart.js: '>=2.8.0' - date-fns: '>=2.0.0' - check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -9053,7 +9044,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -9162,7 +9153,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -10953,7 +10944,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -10987,7 +10978,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.2) - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 eslint: 8.57.1 optionalDependencies: typescript: 5.7.2 @@ -11020,7 +11011,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.2) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 eslint: 8.57.1 tsutils: 3.21.0(typescript@5.7.2) optionalDependencies: @@ -11047,7 +11038,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.1 @@ -11061,7 +11052,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.29.0 '@typescript-eslint/visitor-keys': 8.29.0 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -11909,7 +11900,7 @@ snapshots: capnp-ts@0.7.0: dependencies: - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -11947,11 +11938,6 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-date-fns@3.0.0(chart.js@4.5.0)(date-fns@4.1.0): - dependencies: - chart.js: 4.5.0 - date-fns: 4.1.0 - check-error@2.1.1: {} chokidar@3.6.0: @@ -12249,6 +12235,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + debug@4.4.0(supports-color@9.4.0): dependencies: ms: 2.1.3 @@ -12751,7 +12741,7 @@ snapshots: eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 enhanced-resolve: 5.18.0 eslint: 8.57.1 fast-glob: 3.3.2 @@ -12814,7 +12804,7 @@ snapshots: '@types/doctrine': 0.0.9 '@typescript-eslint/scope-manager': 8.29.0 '@typescript-eslint/utils': 8.29.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 doctrine: 3.0.0 enhanced-resolve: 5.18.0 eslint: 8.57.1 @@ -13094,7 +13084,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -17201,7 +17191,7 @@ snapshots: vite-node@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1): dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) @@ -17299,7 +17289,7 @@ snapshots: '@vitest/spy': 3.1.1 '@vitest/utils': 3.1.1 chai: 5.2.0 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -17368,7 +17358,7 @@ snapshots: vue-eslint-parser@10.1.3(eslint@8.57.1): dependencies: - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 eslint: 8.57.1 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -17418,7 +17408,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.4.0(supports-color@9.4.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color diff --git a/scripts/generate-quickchart-urls.js b/scripts/generate-quickchart-urls.js deleted file mode 100644 index 6c11fd24..00000000 --- a/scripts/generate-quickchart-urls.js +++ /dev/null @@ -1,127 +0,0 @@ -import fetch from 'node-fetch'; - -function formatMonth(dateString) { - const date = new Date(dateString); - return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }); -} - -async function main() { - const res = await fetch('http://localhost:3000/api/chart'); - const { runs } = await res.json(); - - // Sort by date - runs.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at)); - - // Format labels as 'May 2025', 'Jun 2025', etc. - const labels = runs.map(r => formatMonth(r.created_at)); - const orgs = runs.map(r => r.stats.orgs); - const repos = runs.map(r => r.stats.repos); - const prsAndBranches = runs.map(r => r.stats.prsAndBranches); - const commits = runs.map(r => r.stats.commits); - - // Orgs & Repos - const orgsReposConfig = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Organizations', - data: orgs, - borderColor: 'rgb(54, 162, 235)', - fill: false, - }, - { - label: 'Repositories', - data: repos, - borderColor: 'rgb(255, 99, 132)', - fill: false, - } - ] - }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'logarithmic', - title: { display: true, text: 'Count' } - } - } - } - }; - - // PRs & Branches - const prsBranchesConfig = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'PRs & Branches', - data: prsAndBranches, - borderColor: 'rgb(255, 159, 64)', - fill: false, - } - ] - }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'logarithmic', - title: { display: true, text: 'Count' } - } - } - } - }; - - // Commits - const commitsConfig = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Commits', - data: commits, - borderColor: 'rgb(153, 102, 255)', - fill: false, - } - ] - }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'logarithmic', - title: { display: true, text: 'Count' } - } - } - } - }; - - function quickChartUrl(config) { - return 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)); - } - - console.log('\nOrgs & Repos Chart:\n'); - console.log(`![Orgs & Repos](${quickChartUrl(orgsReposConfig)})\n`); - console.log('\nPRs & Branches Chart:\n'); - console.log(`![PRs & Branches](${quickChartUrl(prsBranchesConfig)})\n`); - console.log('\nCommits Chart:\n'); - console.log(`![Commits](${quickChartUrl(commitsConfig)})\n`); -} - -main(); \ No newline at end of file From 0b23b3e31e425c49dd0d8737f58476bd27d2e4ba Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 15:50:20 +0330 Subject: [PATCH 04/10] prettier --- .../app/server/api/chart-image-commits.get.ts | 115 +++++----- .../server/api/chart-image-orgs-repos.get.ts | 131 ++++++----- .../api/chart-image-prs-branches.get.ts | 115 +++++----- packages/app/server/api/chart.get.ts | 215 +++++++++--------- 4 files changed, 296 insertions(+), 280 deletions(-) diff --git a/packages/app/server/api/chart-image-commits.get.ts b/packages/app/server/api/chart-image-commits.get.ts index a64a1284..cecc94ef 100644 --- a/packages/app/server/api/chart-image-commits.get.ts +++ b/packages/app/server/api/chart-image-commits.get.ts @@ -1,61 +1,66 @@ -import { defineEventHandler } from 'h3' -import { $fetch } from 'ofetch' -import chartData from './chart.get' +import { defineEventHandler } from "h3"; +import { $fetch } from "ofetch"; +import chartData from "./chart.get"; interface Run { - created_at: string; - stats: { commits: number } + created_at: string; + stats: { commits: number }; } export default defineEventHandler(async (event) => { - const { runs } = await chartData(event) as { runs: Run[] } - runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at) - return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) - }) - const commits = runs.map((r: Run) => r.stats.commits) - function getMinMax(arr: number[]) { - const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) - const min = Math.min(...filtered) - const max = Math.max(...filtered) - return { min: min - 10, max: max + 10 } - } - const minMax = getMinMax(commits) - const config = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Commits', - data: commits, - borderColor: 'rgb(153, 102, 255)', - fill: false, - } - ] + const { runs } = (await chartData(event)) as { runs: Run[] }; + runs.sort( + (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), + ); + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at); + return date.toLocaleString("en-US", { month: "short", year: "numeric" }); + }); + const commits = runs.map((r: Run) => r.stats.commits); + function getMinMax(arr: number[]) { + const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); + const min = Math.min(...filtered); + const max = Math.max(...filtered); + return { min: min - 10, max: max + 10 }; + } + const minMax = getMinMax(commits); + const config = { + type: "line", + data: { + labels, + datasets: [ + { + label: "Commits", + data: commits, + borderColor: "rgb(153, 102, 255)", + fill: false, }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'linear', - title: { display: true, text: 'Count' }, - min: minMax.min, - max: minMax.max, - ticks: { - stepSize: 10 - } - } - } - } - } - const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' - const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) - event.node.res.setHeader('Content-Type', 'image/png') - return Buffer.from(img) -}) + ], + }, + options: { + responsive: true, + scales: { + x: { + type: "category", + title: { display: true, text: "Month" }, + }, + y: { + type: "linear", + title: { display: true, text: "Count" }, + min: minMax.min, + max: minMax.max, + ticks: { + stepSize: 10, + }, + }, + }, + }, + }; + const quickChartUrl = + "https://quickchart.io/chart?c=" + + encodeURIComponent(JSON.stringify(config)) + + "&format=png&width=800&height=400"; + const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); + event.node.res.setHeader("Content-Type", "image/png"); + return Buffer.from(img); +}); diff --git a/packages/app/server/api/chart-image-orgs-repos.get.ts b/packages/app/server/api/chart-image-orgs-repos.get.ts index b0dcefbf..a70a7115 100644 --- a/packages/app/server/api/chart-image-orgs-repos.get.ts +++ b/packages/app/server/api/chart-image-orgs-repos.get.ts @@ -1,69 +1,74 @@ -import { defineEventHandler } from 'h3' -import { $fetch } from 'ofetch' -import chartData from './chart.get' +import { defineEventHandler } from "h3"; +import { $fetch } from "ofetch"; +import chartData from "./chart.get"; interface Run { - created_at: string; - stats: { orgs: number, repos: number } + created_at: string; + stats: { orgs: number; repos: number }; } export default defineEventHandler(async (event) => { - const { runs } = await chartData(event) as { runs: Run[] } - runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at) - return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) - }) - const orgs = runs.map((r: Run) => r.stats.orgs) - const repos = runs.map((r: Run) => r.stats.repos) - function getMinMax(arr: number[]) { - const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) - const min = Math.min(...filtered) - const max = Math.max(...filtered) - return { min: min - 10, max: max + 10 } - } - const orgsMinMax = getMinMax(orgs) - const reposMinMax = getMinMax(repos) - const config = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Organizations', - data: orgs, - borderColor: 'rgb(54, 162, 235)', - fill: false, - }, - { - label: 'Repositories', - data: repos, - borderColor: 'rgb(255, 99, 132)', - fill: false, - } - ] + const { runs } = (await chartData(event)) as { runs: Run[] }; + runs.sort( + (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), + ); + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at); + return date.toLocaleString("en-US", { month: "short", year: "numeric" }); + }); + const orgs = runs.map((r: Run) => r.stats.orgs); + const repos = runs.map((r: Run) => r.stats.repos); + function getMinMax(arr: number[]) { + const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); + const min = Math.min(...filtered); + const max = Math.max(...filtered); + return { min: min - 10, max: max + 10 }; + } + const orgsMinMax = getMinMax(orgs); + const reposMinMax = getMinMax(repos); + const config = { + type: "line", + data: { + labels, + datasets: [ + { + label: "Organizations", + data: orgs, + borderColor: "rgb(54, 162, 235)", + fill: false, }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'linear', - title: { display: true, text: 'Count' }, - min: Math.min(orgsMinMax.min, reposMinMax.min), - max: Math.max(orgsMinMax.max, reposMinMax.max), - ticks: { - stepSize: 10 - } - } - } - } - } - const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' - const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) - event.node.res.setHeader('Content-Type', 'image/png') - return Buffer.from(img) -}) \ No newline at end of file + { + label: "Repositories", + data: repos, + borderColor: "rgb(255, 99, 132)", + fill: false, + }, + ], + }, + options: { + responsive: true, + scales: { + x: { + type: "category", + title: { display: true, text: "Month" }, + }, + y: { + type: "linear", + title: { display: true, text: "Count" }, + min: Math.min(orgsMinMax.min, reposMinMax.min), + max: Math.max(orgsMinMax.max, reposMinMax.max), + ticks: { + stepSize: 10, + }, + }, + }, + }, + }; + const quickChartUrl = + "https://quickchart.io/chart?c=" + + encodeURIComponent(JSON.stringify(config)) + + "&format=png&width=800&height=400"; + const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); + event.node.res.setHeader("Content-Type", "image/png"); + return Buffer.from(img); +}); diff --git a/packages/app/server/api/chart-image-prs-branches.get.ts b/packages/app/server/api/chart-image-prs-branches.get.ts index 1699204b..704589ec 100644 --- a/packages/app/server/api/chart-image-prs-branches.get.ts +++ b/packages/app/server/api/chart-image-prs-branches.get.ts @@ -1,61 +1,66 @@ -import { defineEventHandler } from 'h3' -import { $fetch } from 'ofetch' -import chartData from './chart.get' +import { defineEventHandler } from "h3"; +import { $fetch } from "ofetch"; +import chartData from "./chart.get"; interface Run { - created_at: string; - stats: { prsAndBranches: number } + created_at: string; + stats: { prsAndBranches: number }; } export default defineEventHandler(async (event) => { - const { runs } = await chartData(event) as { runs: Run[] } - runs.sort((a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at)) - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at) - return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }) - }) - const prsAndBranches = runs.map((r: Run) => r.stats.prsAndBranches) - function getMinMax(arr: number[]) { - const filtered = arr.filter(v => typeof v === 'number' && !isNaN(v)) - const min = Math.min(...filtered) - const max = Math.max(...filtered) - return { min: min - 10, max: max + 10 } - } - const minMax = getMinMax(prsAndBranches) - const config = { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'PRs & Branches', - data: prsAndBranches, - borderColor: 'rgb(255, 159, 64)', - fill: false, - } - ] + const { runs } = (await chartData(event)) as { runs: Run[] }; + runs.sort( + (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), + ); + const labels = runs.map((r: Run) => { + const date = new Date(r.created_at); + return date.toLocaleString("en-US", { month: "short", year: "numeric" }); + }); + const prsAndBranches = runs.map((r: Run) => r.stats.prsAndBranches); + function getMinMax(arr: number[]) { + const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); + const min = Math.min(...filtered); + const max = Math.max(...filtered); + return { min: min - 10, max: max + 10 }; + } + const minMax = getMinMax(prsAndBranches); + const config = { + type: "line", + data: { + labels, + datasets: [ + { + label: "PRs & Branches", + data: prsAndBranches, + borderColor: "rgb(255, 159, 64)", + fill: false, }, - options: { - responsive: true, - scales: { - x: { - type: 'category', - title: { display: true, text: 'Month' } - }, - y: { - type: 'linear', - title: { display: true, text: 'Count' }, - min: minMax.min, - max: minMax.max, - ticks: { - stepSize: 10 - } - } - } - } - } - const quickChartUrl = 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(config)) + '&format=png&width=800&height=400' - const img = await $fetch(quickChartUrl, { responseType: 'arrayBuffer' }) - event.node.res.setHeader('Content-Type', 'image/png') - return Buffer.from(img) -}) \ No newline at end of file + ], + }, + options: { + responsive: true, + scales: { + x: { + type: "category", + title: { display: true, text: "Month" }, + }, + y: { + type: "linear", + title: { display: true, text: "Count" }, + min: minMax.min, + max: minMax.max, + ticks: { + stepSize: 10, + }, + }, + }, + }, + }; + const quickChartUrl = + "https://quickchart.io/chart?c=" + + encodeURIComponent(JSON.stringify(config)) + + "&format=png&width=800&height=400"; + const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); + event.node.res.setHeader("Content-Type", "image/png"); + return Buffer.from(img); +}); diff --git a/packages/app/server/api/chart.get.ts b/packages/app/server/api/chart.get.ts index 2c5e4e14..86f0652f 100644 --- a/packages/app/server/api/chart.get.ts +++ b/packages/app/server/api/chart.get.ts @@ -1,38 +1,38 @@ -import { defineEventHandler, H3Event } from 'h3' -import { useOctokitInstallation } from '../utils/octokit' -import AdmZip from 'adm-zip' +import { defineEventHandler, H3Event } from "h3"; +import { useOctokitInstallation } from "../utils/octokit"; +import AdmZip from "adm-zip"; function extractCountsBlock(content: string): any | null { - const lines = content.split('\n') - let startIdx = -1 - for (let i = 0; i < lines.length; i++) { - if (lines[i].includes('Counts: {')) { - startIdx = i - break - } - } - if (startIdx === -1) return null - // Collect lines until closing } - const blockLines = [] - for (let i = startIdx; i < lines.length; i++) { - blockLines.push(lines[i]) - if (lines[i].trim().endsWith('}')) break - } - // Join and clean up - let block = blockLines.join('\n') - // Remove the leading 'Counts: ' and timestamps - block = block.replace(/.*Counts: /, '') - block = block.replace(/^[^\{]*\{/, '{') // Remove anything before first { - // Remove timestamps at the start of each line - block = block.replace(/^[0-9TZ\-:\.]+Z /gm, '') - // Convert JS object to JSON (add quotes) - block = block.replace(/([a-zA-Z0-9_]+):/g, '"$1":') - try { - return JSON.parse(block) - } catch (e) { - console.error('Failed to parse Counts block as JSON:', block, e) - return null + const lines = content.split("\n"); + let startIdx = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes("Counts: {")) { + startIdx = i; + break; } + } + if (startIdx === -1) return null; + // Collect lines until closing } + const blockLines = []; + for (let i = startIdx; i < lines.length; i++) { + blockLines.push(lines[i]); + if (lines[i].trim().endsWith("}")) break; + } + // Join and clean up + let block = blockLines.join("\n"); + // Remove the leading 'Counts: ' and timestamps + block = block.replace(/.*Counts: /, ""); + block = block.replace(/^[^\{]*\{/, "{"); // Remove anything before first { + // Remove timestamps at the start of each line + block = block.replace(/^[0-9TZ\-:\.]+Z /gm, ""); + // Convert JS object to JSON (add quotes) + block = block.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); + try { + return JSON.parse(block); + } catch (e) { + console.error("Failed to parse Counts block as JSON:", block, e); + return null; + } } let cachedData: any = null; @@ -40,85 +40,86 @@ let lastFetch = 0; const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week in ms export default defineEventHandler(async (event: H3Event) => { - if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { - return cachedData; - } - const owner = 'stackblitz-labs' - const repo = 'pkg.pr.new' - const workflowId = 'stats.yml' + if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { + return cachedData; + } + const owner = "stackblitz-labs"; + const repo = "pkg.pr.new"; + const workflowId = "stats.yml"; - const octokit = await useOctokitInstallation(event, owner, repo) + const octokit = await useOctokitInstallation(event, owner, repo); - const runs = await octokit.paginate( - octokit.rest.actions.listWorkflowRuns, - { - owner, - repo, - workflow_id: workflowId, - per_page: 100, - status: 'success', - } - ) + const runs = await octokit.paginate(octokit.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: workflowId, + per_page: 100, + status: "success", + }); - // Only process the latest 100 runs for performance - const latestRuns = runs.slice(0, 100) + // Only process the latest 100 runs for performance + const latestRuns = runs.slice(0, 100); - // 4. For each run, download logs, unzip, and extract Counts - const results = [] - for (const run of latestRuns) { - let stats = null - try { - const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs({ - owner, - repo, - run_id: run.id - }) - if (logsResponse.status === 302) { - } - if (!logsResponse.data) { - } else { - let buffer - if (Buffer.isBuffer(logsResponse.data)) { - buffer = logsResponse.data - } else if (logsResponse.data instanceof ArrayBuffer) { - buffer = Buffer.from(new Uint8Array(logsResponse.data)) - } else { - throw new Error('logsResponse.data is not a Buffer or ArrayBuffer') - } - const zip = new AdmZip(buffer) - const entries = zip.getEntries() - if (entries.length === 0) { - } - let found = false - for (const entry of entries) { - const content = entry.getData().toString('utf8') - const extracted = extractCountsBlock(content) - if (extracted) { - stats = extracted - found = true - break - } - } - if (!found) { - } - } - } catch (e) { - if (e instanceof Error && e.message && e.message.includes('Server Error')) { - // skip - } else { - // console.error(e) - } + // 4. For each run, download logs, unzip, and extract Counts + const results = []; + for (const run of latestRuns) { + let stats = null; + try { + const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs({ + owner, + repo, + run_id: run.id, + }); + if (logsResponse.status === 302) { + } + if (!logsResponse.data) { + } else { + let buffer; + if (Buffer.isBuffer(logsResponse.data)) { + buffer = logsResponse.data; + } else if (logsResponse.data instanceof ArrayBuffer) { + buffer = Buffer.from(new Uint8Array(logsResponse.data)); + } else { + throw new Error("logsResponse.data is not a Buffer or ArrayBuffer"); + } + const zip = new AdmZip(buffer); + const entries = zip.getEntries(); + if (entries.length === 0) { + } + let found = false; + for (const entry of entries) { + const content = entry.getData().toString("utf8"); + const extracted = extractCountsBlock(content); + if (extracted) { + stats = extracted; + found = true; + break; + } } - if (stats) { - results.push({ - run_number: run.run_number, - created_at: run.created_at, - stats - }) + if (!found) { } + } + } catch (e) { + if ( + e instanceof Error && + e.message && + e.message.includes("Server Error") + ) { + // skip + } else { + // console.error(e) + } } - // 5. Return JSON - cachedData = { runs: results }; - lastFetch = Date.now(); - return cachedData; -}) \ No newline at end of file + if (stats) { + results.push({ + run_number: run.run_number, + created_at: run.created_at, + stats, + }); + } + } + // 5. Return JSON + cachedData = { runs: results }; + lastFetch = Date.now(); + return cachedData; +}); From 64a52ba504d358075276ef3a5ddc420f2fdacca7 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 16:05:12 +0330 Subject: [PATCH 05/10] move apis to ./routes --- packages/app/server/{api => routes}/chart-image-commits.get.ts | 2 +- .../app/server/{api => routes}/chart-image-orgs-repos.get.ts | 2 +- .../app/server/{api => routes}/chart-image-prs-branches.get.ts | 2 +- packages/app/server/{api => routes}/chart.get.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/app/server/{api => routes}/chart-image-commits.get.ts (97%) rename packages/app/server/{api => routes}/chart-image-orgs-repos.get.ts (98%) rename packages/app/server/{api => routes}/chart-image-prs-branches.get.ts (97%) rename packages/app/server/{api => routes}/chart.get.ts (100%) diff --git a/packages/app/server/api/chart-image-commits.get.ts b/packages/app/server/routes/chart-image-commits.get.ts similarity index 97% rename from packages/app/server/api/chart-image-commits.get.ts rename to packages/app/server/routes/chart-image-commits.get.ts index cecc94ef..dfaa59c3 100644 --- a/packages/app/server/api/chart-image-commits.get.ts +++ b/packages/app/server/routes/chart-image-commits.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "./chart.get"; +import chartData from "../api/chart.get"; interface Run { created_at: string; diff --git a/packages/app/server/api/chart-image-orgs-repos.get.ts b/packages/app/server/routes/chart-image-orgs-repos.get.ts similarity index 98% rename from packages/app/server/api/chart-image-orgs-repos.get.ts rename to packages/app/server/routes/chart-image-orgs-repos.get.ts index a70a7115..e7c78b91 100644 --- a/packages/app/server/api/chart-image-orgs-repos.get.ts +++ b/packages/app/server/routes/chart-image-orgs-repos.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "./chart.get"; +import chartData from "../api/chart.get"; interface Run { created_at: string; diff --git a/packages/app/server/api/chart-image-prs-branches.get.ts b/packages/app/server/routes/chart-image-prs-branches.get.ts similarity index 97% rename from packages/app/server/api/chart-image-prs-branches.get.ts rename to packages/app/server/routes/chart-image-prs-branches.get.ts index 704589ec..13508f81 100644 --- a/packages/app/server/api/chart-image-prs-branches.get.ts +++ b/packages/app/server/routes/chart-image-prs-branches.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "./chart.get"; +import chartData from "../api/chart.get"; interface Run { created_at: string; diff --git a/packages/app/server/api/chart.get.ts b/packages/app/server/routes/chart.get.ts similarity index 100% rename from packages/app/server/api/chart.get.ts rename to packages/app/server/routes/chart.get.ts From 5abc922182988ea07a59e0a4ddd49789324085c2 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 16:08:31 +0330 Subject: [PATCH 06/10] trigger ci From 4f1010479247bbf479bc1f181523c230c085279e Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 24 Jul 2025 16:11:28 +0330 Subject: [PATCH 07/10] update --- packages/app/server/routes/chart-image-commits.get.ts | 2 +- packages/app/server/routes/chart-image-orgs-repos.get.ts | 2 +- packages/app/server/routes/chart-image-prs-branches.get.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/server/routes/chart-image-commits.get.ts b/packages/app/server/routes/chart-image-commits.get.ts index dfaa59c3..cecc94ef 100644 --- a/packages/app/server/routes/chart-image-commits.get.ts +++ b/packages/app/server/routes/chart-image-commits.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "../api/chart.get"; +import chartData from "./chart.get"; interface Run { created_at: string; diff --git a/packages/app/server/routes/chart-image-orgs-repos.get.ts b/packages/app/server/routes/chart-image-orgs-repos.get.ts index e7c78b91..a70a7115 100644 --- a/packages/app/server/routes/chart-image-orgs-repos.get.ts +++ b/packages/app/server/routes/chart-image-orgs-repos.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "../api/chart.get"; +import chartData from "./chart.get"; interface Run { created_at: string; diff --git a/packages/app/server/routes/chart-image-prs-branches.get.ts b/packages/app/server/routes/chart-image-prs-branches.get.ts index 13508f81..704589ec 100644 --- a/packages/app/server/routes/chart-image-prs-branches.get.ts +++ b/packages/app/server/routes/chart-image-prs-branches.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler } from "h3"; import { $fetch } from "ofetch"; -import chartData from "../api/chart.get"; +import chartData from "./chart.get"; interface Run { created_at: string; From 6beb81d498aa5c06f0b5d473bc755c6604ba5b59 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 29 Jul 2025 15:03:50 +0330 Subject: [PATCH 08/10] test: using a pure js zip lib --- package.json | 2 - packages/app/package.json | 2 + packages/app/server/routes/chart.get.ts | 167 ++++++++++++++---------- pnpm-lock.yaml | 79 +++++------ 4 files changed, 131 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 6d158412..5ad70106 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "license": "MIT", "devDependencies": { "@jsdevtools/ez-spawn": "^3.0.4", - "@types/adm-zip": "^0.5.7", "@types/node": "^20.14.2", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", @@ -53,7 +52,6 @@ }, "dependencies": { "@cloudflare/vitest-pool-workers": "^0.6.8", - "adm-zip": "^0.5.16", "wrangler": "^3.57.1" }, "packageManager": "pnpm@9.1.3+sha256.7f63001edc077f1cff96cacba901f350796287a2800dfa83fe898f94183e4f5f" diff --git a/packages/app/package.json b/packages/app/package.json index e5a1a14a..d4e0852a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -5,6 +5,7 @@ "scripts": { "build": "nuxi build", "dev": "nuxi dev", + "preview": "nuxi preview", "generate": "nuxi generate", "vendor:octokit": "tsx script/octokit.ts", "prepare": "nuxi prepare", @@ -28,6 +29,7 @@ "@vueuse/core": "^12.2.0", "@vueuse/nuxt": "^12.2.0", "chart.js": "^4.5.0", + "fflate": "^0.8.2", "marked": "^15.0.4", "nuxt": "^3.15.0", "nuxt-shiki": "0.3.0", diff --git a/packages/app/server/routes/chart.get.ts b/packages/app/server/routes/chart.get.ts index 86f0652f..c15e20ea 100644 --- a/packages/app/server/routes/chart.get.ts +++ b/packages/app/server/routes/chart.get.ts @@ -1,6 +1,6 @@ import { defineEventHandler, H3Event } from "h3"; import { useOctokitInstallation } from "../utils/octokit"; -import AdmZip from "adm-zip"; +import { unzipSync, strFromU8 } from "fflate"; function extractCountsBlock(content: string): any | null { const lines = content.split("\n"); @@ -30,7 +30,6 @@ function extractCountsBlock(content: string): any | null { try { return JSON.parse(block); } catch (e) { - console.error("Failed to parse Counts block as JSON:", block, e); return null; } } @@ -40,86 +39,110 @@ let lastFetch = 0; const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week in ms export default defineEventHandler(async (event: H3Event) => { - if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { - return cachedData; - } - const owner = "stackblitz-labs"; - const repo = "pkg.pr.new"; - const workflowId = "stats.yml"; + try { + if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { + return cachedData; + } + const owner = "stackblitz-labs"; + const repo = "pkg.pr.new"; + const workflowId = "stats.yml"; - const octokit = await useOctokitInstallation(event, owner, repo); + const octokit = await useOctokitInstallation(event, owner, repo); - const runs = await octokit.paginate(octokit.rest.actions.listWorkflowRuns, { - owner, - repo, - workflow_id: workflowId, - per_page: 100, - status: "success", - }); + const runs = await octokit.paginate(octokit.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: workflowId, + per_page: 100, + status: "success", + }); - // Only process the latest 100 runs for performance - const latestRuns = runs.slice(0, 100); + // Only process the latest 100 runs for performance + const latestRuns = runs.slice(0, 100); - // 4. For each run, download logs, unzip, and extract Counts - const results = []; - for (const run of latestRuns) { - let stats = null; - try { - const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs({ - owner, - repo, - run_id: run.id, - }); - if (logsResponse.status === 302) { - } - if (!logsResponse.data) { - } else { - let buffer; - if (Buffer.isBuffer(logsResponse.data)) { - buffer = logsResponse.data; - } else if (logsResponse.data instanceof ArrayBuffer) { - buffer = Buffer.from(new Uint8Array(logsResponse.data)); + // 4. For each run, download logs, unzip, and extract Counts + const results = []; + for (const run of latestRuns) { + let stats = null; + try { + const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs( + { + owner, + repo, + run_id: run.id, + }, + ); + if (!logsResponse.data) { } else { - throw new Error("logsResponse.data is not a Buffer or ArrayBuffer"); - } - const zip = new AdmZip(buffer); - const entries = zip.getEntries(); - if (entries.length === 0) { - } - let found = false; - for (const entry of entries) { - const content = entry.getData().toString("utf8"); - const extracted = extractCountsBlock(content); - if (extracted) { - stats = extracted; - found = true; - break; + let zipData; + if (logsResponse.data instanceof ArrayBuffer) { + zipData = new Uint8Array(logsResponse.data); + } else if (logsResponse.data instanceof Uint8Array) { + zipData = logsResponse.data; + } else if ( + typeof Buffer !== "undefined" && + Buffer.isBuffer(logsResponse.data) + ) { + zipData = new Uint8Array( + logsResponse.data.buffer, + logsResponse.data.byteOffset, + logsResponse.data.byteLength, + ); + } else { + throw new Error("logsResponse.data is not a supported binary type"); + } + let files; + try { + files = unzipSync(zipData); + } catch (e) { + continue; + } + let found = false; + for (const [, fileData] of Object.entries(files)) { + let content; + try { + content = strFromU8(fileData); + } catch (e) { + continue; + } + if (typeof content !== "string" || !content) { + continue; + } + let extracted; + try { + extracted = extractCountsBlock(content); + } catch (e) { + continue; + } + if (extracted) { + stats = extracted; + found = true; + break; + } } } - if (!found) { + } catch (e) { + if ( + e instanceof Error && + e.message && + e.message.includes("Server Error") + ) { + // skip } } - } catch (e) { - if ( - e instanceof Error && - e.message && - e.message.includes("Server Error") - ) { - // skip - } else { - // console.error(e) + if (stats) { + results.push({ + run_number: run.run_number, + created_at: run.created_at, + stats, + }); } } - if (stats) { - results.push({ - run_number: run.run_number, - created_at: run.created_at, - stats, - }); - } + // 5. Return JSON + cachedData = { runs: results }; + lastFetch = Date.now(); + return cachedData; + } catch (err) { + return { error: true, message: String(err), stack: (err as any)?.stack }; } - // 5. Return JSON - cachedData = { runs: results }; - lastFetch = Date.now(); - return cachedData; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d6a4881..8c03e10f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,9 +16,6 @@ importers: '@cloudflare/vitest-pool-workers': specifier: ^0.6.8 version: 0.6.16(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) - adm-zip: - specifier: ^0.5.16 - version: 0.5.16 wrangler: specifier: ^3.57.1 version: 3.99.0 @@ -26,9 +23,6 @@ importers: '@jsdevtools/ez-spawn': specifier: ^3.0.4 version: 3.0.4 - '@types/adm-zip': - specifier: ^0.5.7 - version: 0.5.7 '@types/node': specifier: ^20.14.2 version: 20.17.10 @@ -128,6 +122,9 @@ importers: chart.js: specifier: ^4.5.0 version: 4.5.0 + fflate: + specifier: ^0.8.2 + version: 0.8.2 marked: specifier: ^15.0.4 version: 15.0.4 @@ -2709,9 +2706,6 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@types/adm-zip@0.5.7': - resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} - '@types/aws-lambda@8.10.146': resolution: {integrity: sha512-3BaDXYTh0e6UCJYL/jwV/3+GRslSc08toAiZSmleYtkAUyV5rtvdPYxrG/88uqvTuT6sb27WE9OS90ZNTIuQ0g==} @@ -3221,10 +3215,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} - engines: {node: '>=12.0'} - agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -4661,6 +4651,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9412,7 +9405,7 @@ snapshots: semver: 7.7.1 std-env: 3.9.0 tinyexec: 1.0.1 - ufo: 1.5.4 + ufo: 1.6.1 youch: 4.1.0-beta.6 transitivePeerDependencies: - magicast @@ -9572,7 +9565,7 @@ snapshots: pathe: 1.1.2 sirv: 3.0.0 tinyglobby: 0.2.12 - ufo: 1.5.4 + ufo: 1.6.1 unifont: 0.1.7 unplugin: 2.2.2 unstorage: 1.16.0(db0@0.2.1)(ioredis@5.4.2) @@ -9643,7 +9636,7 @@ snapshots: scule: 1.3.0 semver: 7.7.1 std-env: 3.9.0 - ufo: 1.5.4 + ufo: 1.6.1 unctx: 2.4.1 unimport: 4.1.3 untyped: 1.5.2 @@ -9689,7 +9682,7 @@ snapshots: pkg-types: 1.3.1 scule: 1.3.0 std-env: 3.9.0 - ufo: 1.5.4 + ufo: 1.6.1 uncrypto: 0.1.3 unimport: 3.14.5(rollup@4.29.1) untyped: 1.5.2 @@ -9834,7 +9827,7 @@ snapshots: postcss: 8.5.3 rollup-plugin-visualizer: 5.14.0(rollup@4.29.1) std-env: 3.9.0 - ufo: 1.5.4 + ufo: 1.6.1 unenv: 1.10.0 unplugin: 2.2.2 vite: 6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) @@ -10855,10 +10848,6 @@ snapshots: '@trysound/sax@0.2.0': {} - '@types/adm-zip@0.5.7': - dependencies: - '@types/node': 20.17.10 - '@types/aws-lambda@8.10.146': {} '@types/color-convert@2.0.4': @@ -11537,8 +11526,6 @@ snapshots: acorn@8.14.1: {} - adm-zip@0.5.16: {} - agent-base@7.1.3: {} ajv-formats@2.1.1(ajv@8.17.1): @@ -12696,10 +12683,10 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -12709,9 +12696,9 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-config-prettier: 8.10.0(eslint@8.57.1) - eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-node: 11.1.0(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -12738,7 +12725,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -12750,7 +12737,7 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) eslint-plugin-import-x: 4.6.1(eslint@8.57.1)(typescript@5.7.2) transitivePeerDependencies: - supports-color @@ -12765,14 +12752,14 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -12819,7 +12806,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -12830,7 +12817,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13282,6 +13269,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -13345,7 +13334,7 @@ snapshots: magic-regexp: 0.8.0 magic-string: 0.30.17 pathe: 1.1.2 - ufo: 1.5.4 + ufo: 1.6.1 unplugin: 1.16.0 transitivePeerDependencies: - encoding @@ -13609,7 +13598,7 @@ snapshots: iron-webcrypto: 1.2.1 node-mock-http: 1.0.0 radix3: 1.1.2(patch_hash=gsdwrxpd7rn5ueaa55ufykpidm) - ufo: 1.5.4 + ufo: 1.6.1 uncrypto: 0.1.3 h3@1.15.3: @@ -14266,7 +14255,7 @@ snapshots: node-forge: 1.3.1 pathe: 1.1.2 std-env: 3.8.0 - ufo: 1.5.4 + ufo: 1.6.1 untun: 0.1.3 uqr: 0.1.2 @@ -14330,7 +14319,7 @@ snapshots: mlly: 1.7.4 regexp-tree: 0.1.27 type-level-regexp: 0.1.17 - ufo: 1.5.4 + ufo: 1.6.1 unplugin: 1.16.0 magic-string-ast@0.7.1: @@ -14916,7 +14905,7 @@ snapshots: serve-placeholder: 2.0.2 serve-static: 1.16.2 std-env: 3.8.0 - ufo: 1.5.4 + ufo: 1.6.1 uncrypto: 0.1.3 unctx: 2.4.1 unenv: 1.10.0 @@ -15152,7 +15141,7 @@ snapshots: execa: 8.0.1 pathe: 1.1.2 pkg-types: 1.3.1 - ufo: 1.5.4 + ufo: 1.6.1 nypm@0.4.1: dependencies: @@ -15161,7 +15150,7 @@ snapshots: pathe: 1.1.2 pkg-types: 1.3.1 tinyexec: 0.3.2 - ufo: 1.5.4 + ufo: 1.6.1 nypm@0.5.4: dependencies: @@ -15170,7 +15159,7 @@ snapshots: pathe: 2.0.3 pkg-types: 1.3.1 tinyexec: 0.3.2 - ufo: 1.5.4 + ufo: 1.6.1 nypm@0.6.0: dependencies: @@ -15234,7 +15223,7 @@ snapshots: dependencies: destr: 2.0.3 node-fetch-native: 1.6.4 - ufo: 1.5.4 + ufo: 1.6.1 ohash@1.1.4: {} @@ -17343,7 +17332,7 @@ snapshots: vue-bundle-renderer@2.1.1: dependencies: - ufo: 1.5.4 + ufo: 1.6.1 vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.13(typescript@5.7.2)): dependencies: From a2b7498dc0e5ad63f102e4adf86fc24886f77386 Mon Sep 17 00:00:00 2001 From: AmirHossein Sakhravi Date: Tue, 29 Jul 2025 15:08:25 +0330 Subject: [PATCH 09/10] test: charts in readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e57d5f19..73240385 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ Show that your project uses pkg.pr.new by adding a badge to your README:

Growth Statistics
- Orgs & Repos - PRs & Branches - Commits + Orgs & Repos + PRs & Branches + Commits

### How to Get a Badge for Your Repository From f5a333f49cf0bb94fd482fb925114338dc0f3e33 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 2 Aug 2025 13:51:10 +0330 Subject: [PATCH 10/10] update --- packages/app/app/pages/~/growth.vue | 562 ++++++++++++++++++ packages/app/package.json | 2 + .../app/server/{routes => api}/chart.get.ts | 0 .../server/routes/chart-image-commits.get.ts | 66 -- .../routes/chart-image-orgs-repos.get.ts | 74 --- .../routes/chart-image-prs-branches.get.ts | 66 -- .../app/server/routes/chart-old-api.get.ts | 148 +++++ pnpm-lock.yaml | 146 ++++- 8 files changed, 833 insertions(+), 231 deletions(-) create mode 100644 packages/app/app/pages/~/growth.vue rename packages/app/server/{routes => api}/chart.get.ts (100%) delete mode 100644 packages/app/server/routes/chart-image-commits.get.ts delete mode 100644 packages/app/server/routes/chart-image-orgs-repos.get.ts delete mode 100644 packages/app/server/routes/chart-image-prs-branches.get.ts create mode 100644 packages/app/server/routes/chart-old-api.get.ts diff --git a/packages/app/app/pages/~/growth.vue b/packages/app/app/pages/~/growth.vue new file mode 100644 index 00000000..9f6d4246 --- /dev/null +++ b/packages/app/app/pages/~/growth.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/packages/app/package.json b/packages/app/package.json index d4e0852a..8e430027 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -29,6 +29,8 @@ "@vueuse/core": "^12.2.0", "@vueuse/nuxt": "^12.2.0", "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-rough": "^0.2.0", "fflate": "^0.8.2", "marked": "^15.0.4", "nuxt": "^3.15.0", diff --git a/packages/app/server/routes/chart.get.ts b/packages/app/server/api/chart.get.ts similarity index 100% rename from packages/app/server/routes/chart.get.ts rename to packages/app/server/api/chart.get.ts diff --git a/packages/app/server/routes/chart-image-commits.get.ts b/packages/app/server/routes/chart-image-commits.get.ts deleted file mode 100644 index cecc94ef..00000000 --- a/packages/app/server/routes/chart-image-commits.get.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { defineEventHandler } from "h3"; -import { $fetch } from "ofetch"; -import chartData from "./chart.get"; - -interface Run { - created_at: string; - stats: { commits: number }; -} - -export default defineEventHandler(async (event) => { - const { runs } = (await chartData(event)) as { runs: Run[] }; - runs.sort( - (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), - ); - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at); - return date.toLocaleString("en-US", { month: "short", year: "numeric" }); - }); - const commits = runs.map((r: Run) => r.stats.commits); - function getMinMax(arr: number[]) { - const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); - const min = Math.min(...filtered); - const max = Math.max(...filtered); - return { min: min - 10, max: max + 10 }; - } - const minMax = getMinMax(commits); - const config = { - type: "line", - data: { - labels, - datasets: [ - { - label: "Commits", - data: commits, - borderColor: "rgb(153, 102, 255)", - fill: false, - }, - ], - }, - options: { - responsive: true, - scales: { - x: { - type: "category", - title: { display: true, text: "Month" }, - }, - y: { - type: "linear", - title: { display: true, text: "Count" }, - min: minMax.min, - max: minMax.max, - ticks: { - stepSize: 10, - }, - }, - }, - }, - }; - const quickChartUrl = - "https://quickchart.io/chart?c=" + - encodeURIComponent(JSON.stringify(config)) + - "&format=png&width=800&height=400"; - const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); - event.node.res.setHeader("Content-Type", "image/png"); - return Buffer.from(img); -}); diff --git a/packages/app/server/routes/chart-image-orgs-repos.get.ts b/packages/app/server/routes/chart-image-orgs-repos.get.ts deleted file mode 100644 index a70a7115..00000000 --- a/packages/app/server/routes/chart-image-orgs-repos.get.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { defineEventHandler } from "h3"; -import { $fetch } from "ofetch"; -import chartData from "./chart.get"; - -interface Run { - created_at: string; - stats: { orgs: number; repos: number }; -} - -export default defineEventHandler(async (event) => { - const { runs } = (await chartData(event)) as { runs: Run[] }; - runs.sort( - (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), - ); - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at); - return date.toLocaleString("en-US", { month: "short", year: "numeric" }); - }); - const orgs = runs.map((r: Run) => r.stats.orgs); - const repos = runs.map((r: Run) => r.stats.repos); - function getMinMax(arr: number[]) { - const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); - const min = Math.min(...filtered); - const max = Math.max(...filtered); - return { min: min - 10, max: max + 10 }; - } - const orgsMinMax = getMinMax(orgs); - const reposMinMax = getMinMax(repos); - const config = { - type: "line", - data: { - labels, - datasets: [ - { - label: "Organizations", - data: orgs, - borderColor: "rgb(54, 162, 235)", - fill: false, - }, - { - label: "Repositories", - data: repos, - borderColor: "rgb(255, 99, 132)", - fill: false, - }, - ], - }, - options: { - responsive: true, - scales: { - x: { - type: "category", - title: { display: true, text: "Month" }, - }, - y: { - type: "linear", - title: { display: true, text: "Count" }, - min: Math.min(orgsMinMax.min, reposMinMax.min), - max: Math.max(orgsMinMax.max, reposMinMax.max), - ticks: { - stepSize: 10, - }, - }, - }, - }, - }; - const quickChartUrl = - "https://quickchart.io/chart?c=" + - encodeURIComponent(JSON.stringify(config)) + - "&format=png&width=800&height=400"; - const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); - event.node.res.setHeader("Content-Type", "image/png"); - return Buffer.from(img); -}); diff --git a/packages/app/server/routes/chart-image-prs-branches.get.ts b/packages/app/server/routes/chart-image-prs-branches.get.ts deleted file mode 100644 index 704589ec..00000000 --- a/packages/app/server/routes/chart-image-prs-branches.get.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { defineEventHandler } from "h3"; -import { $fetch } from "ofetch"; -import chartData from "./chart.get"; - -interface Run { - created_at: string; - stats: { prsAndBranches: number }; -} - -export default defineEventHandler(async (event) => { - const { runs } = (await chartData(event)) as { runs: Run[] }; - runs.sort( - (a: Run, b: Run) => Date.parse(a.created_at) - Date.parse(b.created_at), - ); - const labels = runs.map((r: Run) => { - const date = new Date(r.created_at); - return date.toLocaleString("en-US", { month: "short", year: "numeric" }); - }); - const prsAndBranches = runs.map((r: Run) => r.stats.prsAndBranches); - function getMinMax(arr: number[]) { - const filtered = arr.filter((v) => typeof v === "number" && !isNaN(v)); - const min = Math.min(...filtered); - const max = Math.max(...filtered); - return { min: min - 10, max: max + 10 }; - } - const minMax = getMinMax(prsAndBranches); - const config = { - type: "line", - data: { - labels, - datasets: [ - { - label: "PRs & Branches", - data: prsAndBranches, - borderColor: "rgb(255, 159, 64)", - fill: false, - }, - ], - }, - options: { - responsive: true, - scales: { - x: { - type: "category", - title: { display: true, text: "Month" }, - }, - y: { - type: "linear", - title: { display: true, text: "Count" }, - min: minMax.min, - max: minMax.max, - ticks: { - stepSize: 10, - }, - }, - }, - }, - }; - const quickChartUrl = - "https://quickchart.io/chart?c=" + - encodeURIComponent(JSON.stringify(config)) + - "&format=png&width=800&height=400"; - const img = await $fetch(quickChartUrl, { responseType: "arrayBuffer" }); - event.node.res.setHeader("Content-Type", "image/png"); - return Buffer.from(img); -}); diff --git a/packages/app/server/routes/chart-old-api.get.ts b/packages/app/server/routes/chart-old-api.get.ts new file mode 100644 index 00000000..c15e20ea --- /dev/null +++ b/packages/app/server/routes/chart-old-api.get.ts @@ -0,0 +1,148 @@ +import { defineEventHandler, H3Event } from "h3"; +import { useOctokitInstallation } from "../utils/octokit"; +import { unzipSync, strFromU8 } from "fflate"; + +function extractCountsBlock(content: string): any | null { + const lines = content.split("\n"); + let startIdx = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes("Counts: {")) { + startIdx = i; + break; + } + } + if (startIdx === -1) return null; + // Collect lines until closing } + const blockLines = []; + for (let i = startIdx; i < lines.length; i++) { + blockLines.push(lines[i]); + if (lines[i].trim().endsWith("}")) break; + } + // Join and clean up + let block = blockLines.join("\n"); + // Remove the leading 'Counts: ' and timestamps + block = block.replace(/.*Counts: /, ""); + block = block.replace(/^[^\{]*\{/, "{"); // Remove anything before first { + // Remove timestamps at the start of each line + block = block.replace(/^[0-9TZ\-:\.]+Z /gm, ""); + // Convert JS object to JSON (add quotes) + block = block.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); + try { + return JSON.parse(block); + } catch (e) { + return null; + } +} + +let cachedData: any = null; +let lastFetch = 0; +const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week in ms + +export default defineEventHandler(async (event: H3Event) => { + try { + if (cachedData && Date.now() - lastFetch < CACHE_DURATION) { + return cachedData; + } + const owner = "stackblitz-labs"; + const repo = "pkg.pr.new"; + const workflowId = "stats.yml"; + + const octokit = await useOctokitInstallation(event, owner, repo); + + const runs = await octokit.paginate(octokit.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: workflowId, + per_page: 100, + status: "success", + }); + + // Only process the latest 100 runs for performance + const latestRuns = runs.slice(0, 100); + + // 4. For each run, download logs, unzip, and extract Counts + const results = []; + for (const run of latestRuns) { + let stats = null; + try { + const logsResponse = await octokit.rest.actions.downloadWorkflowRunLogs( + { + owner, + repo, + run_id: run.id, + }, + ); + if (!logsResponse.data) { + } else { + let zipData; + if (logsResponse.data instanceof ArrayBuffer) { + zipData = new Uint8Array(logsResponse.data); + } else if (logsResponse.data instanceof Uint8Array) { + zipData = logsResponse.data; + } else if ( + typeof Buffer !== "undefined" && + Buffer.isBuffer(logsResponse.data) + ) { + zipData = new Uint8Array( + logsResponse.data.buffer, + logsResponse.data.byteOffset, + logsResponse.data.byteLength, + ); + } else { + throw new Error("logsResponse.data is not a supported binary type"); + } + let files; + try { + files = unzipSync(zipData); + } catch (e) { + continue; + } + let found = false; + for (const [, fileData] of Object.entries(files)) { + let content; + try { + content = strFromU8(fileData); + } catch (e) { + continue; + } + if (typeof content !== "string" || !content) { + continue; + } + let extracted; + try { + extracted = extractCountsBlock(content); + } catch (e) { + continue; + } + if (extracted) { + stats = extracted; + found = true; + break; + } + } + } + } catch (e) { + if ( + e instanceof Error && + e.message && + e.message.includes("Server Error") + ) { + // skip + } + } + if (stats) { + results.push({ + run_number: run.run_number, + created_at: run.created_at, + stats, + }); + } + } + // 5. Return JSON + cachedData = { runs: results }; + lastFetch = Date.now(); + return cachedData; + } catch (err) { + return { error: true, message: String(err), stack: (err as any)?.stack }; + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c03e10f..2b5e3ccb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,12 @@ importers: chart.js: specifier: ^4.5.0 version: 4.5.0 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.0)(date-fns@4.1.0) + chartjs-plugin-rough: + specifier: ^0.2.0 + version: 0.2.0(chart.js@4.5.0)(roughjs@4.6.6) fflate: specifier: ^0.8.2 version: 0.8.2 @@ -161,7 +167,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.12.1 - version: 3.16.0(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) + version: 3.16.0(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) '@cloudflare/workers-types': specifier: ^4.20240512.0 version: 4.20241218.0 @@ -3568,6 +3574,18 @@ packages: resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} engines: {pnpm: '>=8'} + chartjs-adapter-date-fns@3.0.0: + resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} + peerDependencies: + chart.js: '>=2.8.0' + date-fns: '>=2.0.0' + + chartjs-plugin-rough@0.2.0: + resolution: {integrity: sha512-9BMkqr9btzwW7n+a1ognV44XmarBDdOMnSEpdudAcPzovQWAUqFibgwIZYLAb4D3Er1VBwCPdf61YC9HKGTi8w==} + peerDependencies: + chart.js: '>= 2.7.0 < 3' + roughjs: ^2.0.1 + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -4907,6 +4925,9 @@ packages: h3@1.15.3: resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -6220,6 +6241,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -6306,6 +6330,12 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -6862,6 +6892,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -8211,7 +8244,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1))': + '@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1))': dependencies: '@antfu/install-pkg': 1.0.0 '@clack/prompts': 0.9.1 @@ -8220,7 +8253,7 @@ snapshots: '@stylistic/eslint-plugin': 2.13.0(eslint@8.57.1)(typescript@5.7.2) '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) '@typescript-eslint/parser': 8.29.0(eslint@8.57.1)(typescript@5.7.2) - '@vitest/eslint-plugin': 1.1.39(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) + '@vitest/eslint-plugin': 1.1.39(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) eslint: 8.57.1 eslint-config-flat-gitignore: 1.0.1(eslint@8.57.1) eslint-flat-config-utils: 1.1.0 @@ -9037,7 +9070,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -9146,7 +9179,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -10933,7 +10966,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -10967,7 +11000,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) eslint: 8.57.1 optionalDependencies: typescript: 5.7.2 @@ -11000,7 +11033,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.2) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) eslint: 8.57.1 tsutils: 3.21.0(typescript@5.7.2) optionalDependencies: @@ -11027,7 +11060,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.1 @@ -11041,7 +11074,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.29.0 '@typescript-eslint/visitor-keys': 8.29.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -11183,13 +11216,13 @@ snapshots: publint: 0.2.12 semver: 7.6.3 - '@vitest/eslint-plugin@1.1.39(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1))': + '@vitest/eslint-plugin@1.1.39(@typescript-eslint/utils@8.29.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)(vitest@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1))': dependencies: '@typescript-eslint/utils': 8.29.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 optionalDependencies: typescript: 5.7.2 - vitest: 3.1.1(@types/debug@4.1.12)(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) + vitest: 3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) '@vitest/expect@3.1.1': dependencies: @@ -11887,7 +11920,7 @@ snapshots: capnp-ts@0.7.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -11925,6 +11958,16 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 + chartjs-adapter-date-fns@3.0.0(chart.js@4.5.0)(date-fns@4.1.0): + dependencies: + chart.js: 4.5.0 + date-fns: 4.1.0 + + chartjs-plugin-rough@0.2.0(chart.js@4.5.0)(roughjs@4.6.6): + dependencies: + chart.js: 4.5.0 + roughjs: 4.6.6 + check-error@2.1.1: {} chokidar@3.6.0: @@ -12222,10 +12265,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.0(supports-color@9.4.0): dependencies: ms: 2.1.3 @@ -12728,7 +12767,7 @@ snapshots: eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) enhanced-resolve: 5.18.0 eslint: 8.57.1 fast-glob: 3.3.2 @@ -12791,7 +12830,7 @@ snapshots: '@types/doctrine': 0.0.9 '@typescript-eslint/scope-manager': 8.29.0 '@typescript-eslint/utils': 8.29.0(eslint@8.57.1)(typescript@5.7.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) doctrine: 3.0.0 enhanced-resolve: 5.18.0 eslint: 8.57.1 @@ -13071,7 +13110,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -13613,6 +13652,8 @@ snapshots: ufo: 1.6.1 uncrypto: 0.1.3 + hachure-fill@0.5.2: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -15404,6 +15445,8 @@ snapshots: path-browserify@1.0.1: {} + path-data-parser@0.1.0: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -15467,6 +15510,13 @@ snapshots: pluralize@8.0.0: {} + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + possible-typed-array-names@1.0.0: {} postcss-calc@10.0.2(postcss@8.5.3): @@ -16128,6 +16178,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.39.0 fsevents: 2.3.3 + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + run-applescript@7.0.0: {} run-parallel@1.2.0: @@ -16858,7 +16915,7 @@ snapshots: mlly: 1.7.4 ohash: 1.1.4 pathe: 1.1.2 - ufo: 1.5.4 + ufo: 1.6.1 unhead@1.11.14: dependencies: @@ -17180,7 +17237,7 @@ snapshots: vite-node@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) @@ -17278,7 +17335,7 @@ snapshots: '@vitest/spy': 3.1.1 '@vitest/utils': 3.1.1 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -17307,6 +17364,45 @@ snapshots: - tsx - yaml + vitest@3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1): + dependencies: + '@vitest/expect': 3.1.1 + '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1)) + '@vitest/pretty-format': 3.1.1 + '@vitest/runner': 3.1.1 + '@vitest/snapshot': 3.1.1 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 + chai: 5.2.0 + debug: 4.4.0(supports-color@9.4.0) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.2.5(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) + vite-node: 3.1.1(@types/node@20.17.10)(jiti@2.4.2)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.10 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + optional: true + vscode-jsonrpc@6.0.0: {} vscode-languageclient@7.0.0: @@ -17347,7 +17443,7 @@ snapshots: vue-eslint-parser@10.1.3(eslint@8.57.1): dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) eslint: 8.57.1 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -17397,7 +17493,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color