From c6977cd76023ae0bc6c737d73a97d00425bd8fab Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:29:42 -0700 Subject: [PATCH] github api --- packages/cubby/.env.example | 1 + packages/cubby/CLAUDE.md | 111 ++++++++++++++++++++++++++++++++++ packages/cubby/main | 0 packages/cubby/src/server.tsx | 107 ++++++++++++++++++++++++++++---- 4 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 packages/cubby/.env.example create mode 100644 packages/cubby/CLAUDE.md delete mode 100644 packages/cubby/main diff --git a/packages/cubby/.env.example b/packages/cubby/.env.example new file mode 100644 index 0000000..850baac --- /dev/null +++ b/packages/cubby/.env.example @@ -0,0 +1 @@ +GITHUB_TOKEN=your_github_token_here \ No newline at end of file diff --git a/packages/cubby/CLAUDE.md b/packages/cubby/CLAUDE.md new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/packages/cubby/CLAUDE.md @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/packages/cubby/main b/packages/cubby/main deleted file mode 100644 index e69de29..0000000 diff --git a/packages/cubby/src/server.tsx b/packages/cubby/src/server.tsx index 354b000..aa8ebb4 100644 --- a/packages/cubby/src/server.tsx +++ b/packages/cubby/src/server.tsx @@ -1,7 +1,6 @@ import { Hono } from "hono" import { serveStatic } from "hono/bun" import { render } from "preact-render-to-string" -import { $ } from "bun" import { readdirSync } from "fs" import { mkdir, readdir } from 'node:fs/promises' import { basename, join } from 'path' @@ -141,7 +140,15 @@ app.delete('/p/:id/delete/:filename', async c => { async function projects(): Promise { const subdirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }) .filter((entry: any) => entry.isDirectory()) - .map((entry: any) => entry.name).sort() + .map((entry: any) => entry.name) + .filter(name => { + try { + return Bun.file(`${PROJECTS_DIR}/${name}/package.json`).size > 0 + } catch { + return false + } + }) + .sort() return subdirs } @@ -162,24 +169,102 @@ async function isWorkshopApp(projectName: string): Promise { return json.scripts && json.scripts["subdomain:start"] !== undefined } -async function projectsWithDates(): Promise<{ name: string, mtime: Date, status: "recent" | "active" | "inactive" }[]> { +type ProjectDate = { + name: string + mtime: Date + status: "recent" | "active" | "inactive" +} + +async function projectsWithDates(): Promise { const names = await projects() - const projectsWithDates = await Promise.all(names.map(async name => { - const lastModified = await mtime(name) + + // Try to batch GitHub API calls for better performance + const dates = await batchedMtimes(names) + + const projectsWithDates = names.map((name, index) => { + const lastModified = dates[index] || new Date(0) const daysSince = (Date.now() - lastModified.getTime()) / (24 * 60 * 60 * 1000) - const status: "recent" | "active" | "inactive" = daysSince <= 14 ? "recent" : daysSince <= 30 ? "active" : "inactive" + const status: ProjectDate["status"] = daysSince <= 14 ? "recent" : daysSince <= 30 ? "active" : "inactive" return { name, mtime: lastModified, status } - })) + }).filter((project): project is ProjectDate => project.mtime instanceof Date) + const sorted = projectsWithDates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()) return sorted } -async function mtime(project: string): Promise { - const proc = await $`cd ${PROJECTS_DIR} && git log -1 --format=%ct -- ${project}`.quiet() - if (proc.exitCode !== 0) return new Date(0) - return new Date(parseInt(proc.stdout.toString().trim()) * 1000) +// Cache to avoid redundant API calls +const mtimeCache = new Map() +const CACHE_TTL = 5 * 60 * 1000 // 5 minutes + +async function batchedMtimes(projects: string[]): Promise { + const token = process.env.GITHUB_TOKEN + if (!token) { + console.warn('GITHUB_TOKEN not set, returning default dates') + return projects.map(() => new Date(0)) + } + + // Check cache for all projects first + const results: (Date | null)[] = new Array(projects.length).fill(null) + const uncachedProjects: { index: number, name: string }[] = [] + + projects.forEach((project, index) => { + const cached = mtimeCache.get(project) + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + results[index] = cached.date + } else { + uncachedProjects.push({ index, name: project }) + } + }) + + if (uncachedProjects.length === 0) { + return results as Date[] + } + + try { + // Batch API calls with Promise.all for parallel execution + const apiResults = await Promise.all(uncachedProjects.map(async ({ index, name }) => { + try { + console.log(`Fetching GitHub API for ${name}`) + const response = await fetch(`https://api.github.com/repos/probablycorey/the-workshop/commits?path=packages/${encodeURIComponent(name)}&per_page=1`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'cubby-workshop-app' + } + }) + + if (!response.ok) { + console.warn(`GitHub API error for ${name}: ${response.status}`) + return { index, date: new Date(0) } + } + + const commits = await response.json() + if (!commits || commits.length === 0) { + return { index, date: new Date(0) } + } + + const date = new Date(commits[0].commit.committer.date) + mtimeCache.set(name, { date, timestamp: Date.now() }) + return { index, date } + } catch (error) { + console.warn(`Error fetching GitHub data for ${name}:`, error) + return { index, date: new Date(0) } + } + })) + + // Fill in the results array + apiResults.forEach(({ index, date }) => { + results[index] = date + }) + + return results as Date[] + } catch (error) { + console.warn('Batch GitHub API call failed, returning default dates:', error) + return projects.map(() => new Date(0)) + } } + function tsx(node: any) { return "" + render({node}) }