github api
This commit is contained in:
parent
eac2e4e009
commit
c6977cd760
1
packages/cubby/.env.example
Normal file
1
packages/cubby/.env.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
GITHUB_TOKEN=your_github_token_here
|
||||
111
packages/cubby/CLAUDE.md
Normal file
111
packages/cubby/CLAUDE.md
Normal file
|
|
@ -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 <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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 <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||
|
|
@ -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<string[]> {
|
||||
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<boolean> {
|
|||
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<ProjectDate[]> {
|
||||
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<Date> {
|
||||
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<string, { date: Date, timestamp: number }>()
|
||||
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
async function batchedMtimes(projects: string[]): Promise<Date[]> {
|
||||
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 "<!DOCTYPE html>" + render(<Layout>{node}</Layout>)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user