snapshot-github-traffic.mjs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { mkdir, readFile, writeFile } from 'node:fs/promises'
  2. import { dirname, join, resolve } from 'node:path'
  3. import { spawnSync } from 'node:child_process'
  4. import { fileURLToPath } from 'node:url'
  5. const scriptDir = dirname(fileURLToPath(import.meta.url))
  6. const sourceRoot = resolve(scriptDir, '..')
  7. const args = process.argv.slice(2)
  8. const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
  9. const branchRoles = await readJson(join(sourceRoot, 'ecosystem', 'branch-roles.json'))
  10. const optionValue = name => {
  11. const prefix = `${name}=`
  12. const match = args.find(arg => arg.startsWith(prefix))
  13. return match ? match.slice(prefix.length) : null
  14. }
  15. const repoSlugFromUrl = url => {
  16. const match = String(url).match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/)
  17. if (!match) {
  18. throw new Error(`Unable to parse GitHub repository URL: ${url}`)
  19. }
  20. return match[1]
  21. }
  22. const repo = optionValue('--repo') || repoSlugFromUrl(branchRoles.publicMainRepository.github)
  23. const output = optionValue('--output')
  24. const jsonOutput = args.includes('--json')
  25. const runGhJson = commandArgs => {
  26. const result = spawnSync('gh', commandArgs, {
  27. cwd: sourceRoot,
  28. encoding: 'utf8',
  29. stdio: 'pipe'
  30. })
  31. if (result.status !== 0) {
  32. const details = [result.stderr, result.stdout].filter(Boolean).join('\n')
  33. throw new Error(`Command failed: gh ${commandArgs.join(' ')}${details ? `\n${details}` : ''}`)
  34. }
  35. return JSON.parse(result.stdout || '{}')
  36. }
  37. const api = path => runGhJson(['api', `repos/${repo}${path}`])
  38. const [repoInfo, views, clones, referrers, paths] = [
  39. api(''),
  40. api('/traffic/views'),
  41. api('/traffic/clones'),
  42. api('/traffic/popular/referrers'),
  43. api('/traffic/popular/paths')
  44. ]
  45. const snapshot = {
  46. generatedAt: new Date().toISOString(),
  47. repo,
  48. stars: repoInfo.stargazers_count,
  49. forks: repoInfo.forks_count,
  50. openIssues: repoInfo.open_issues_count,
  51. watchers: repoInfo.subscribers_count,
  52. views,
  53. clones,
  54. referrers,
  55. paths
  56. }
  57. const table = (headers, rows) => {
  58. const line = `| ${headers.join(' | ')} |`
  59. const divider = `| ${headers.map(() => '---').join(' | ')} |`
  60. return [line, divider, ...rows.map(row => `| ${row.join(' | ')} |`)].join('\n')
  61. }
  62. const renderMarkdown = data => {
  63. const referrerRows = data.referrers.length
  64. ? data.referrers.map(item => [item.referrer, item.count, item.uniques])
  65. : [['-', '-', '-']]
  66. const pathRows = data.paths.length
  67. ? data.paths.map(item => [item.path, item.title || '-', item.count, item.uniques])
  68. : [['-', '-', '-', '-']]
  69. return [
  70. '# GitHub Traffic Snapshot',
  71. '',
  72. `- Generated at: ${data.generatedAt}`,
  73. `- Repository: ${data.repo}`,
  74. `- Stars: ${data.stars}`,
  75. `- Forks: ${data.forks}`,
  76. `- Open issues: ${data.openIssues}`,
  77. `- Watchers: ${data.watchers}`,
  78. `- Views: ${data.views.count} total / ${data.views.uniques} unique`,
  79. `- Clones: ${data.clones.count} total / ${data.clones.uniques} unique`,
  80. '',
  81. '## Referrers',
  82. '',
  83. table(['Referrer', 'Views', 'Unique visitors'], referrerRows),
  84. '',
  85. '## Popular Content',
  86. '',
  87. table(['Path', 'Title', 'Views', 'Unique visitors'], pathRows),
  88. ''
  89. ].join('\n')
  90. }
  91. const body = jsonOutput ? `${JSON.stringify(snapshot, null, 2)}\n` : renderMarkdown(snapshot)
  92. if (output) {
  93. const outputPath = resolve(sourceRoot, output)
  94. await mkdir(dirname(outputPath), { recursive: true })
  95. await writeFile(outputPath, body)
  96. console.log(`Wrote ${outputPath.replace(`${sourceRoot}/`, '')}`)
  97. } else {
  98. process.stdout.write(body)
  99. }