diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index da5083df2..04c7830db 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -28,6 +28,8 @@ jobs: - name: Build site run: pnpm --filter docs build + env: + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload build artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 9697e305a..db119f9d0 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -30,6 +30,8 @@ jobs: - name: Build site run: pnpm --filter docs build + env: + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy to Cloudflare Pages uses: AdrianGonz97/refined-cf-pages-action@v1 diff --git a/docs/src/routes/docs/showcase/+page.md b/docs/src/routes/docs/showcase/+page.md index 14971a20a..5cf35c909 100644 --- a/docs/src/routes/docs/showcase/+page.md +++ b/docs/src/routes/docs/showcase/+page.md @@ -1,33 +1,26 @@ # Showcase -
- {#each sites as site} -
- - {site.name} - - {#if site.description} -

{site.description}

- {/if} -
- {#if site.source} -
-
- {/each} -
- -[More](https://github.com/techniq/layerchart/network/dependents) +## Featured + + + +## [Supporters](https://github.com/techniq/layerchart?tab=readme-ov-file#sponsors) + + + +## Popular + + + +## Other + + + +## [More](https://github.com/techniq/layerchart/network/dependents) diff --git a/docs/src/routes/docs/showcase/Showcase.svelte b/docs/src/routes/docs/showcase/Showcase.svelte new file mode 100644 index 000000000..bc8276d1e --- /dev/null +++ b/docs/src/routes/docs/showcase/Showcase.svelte @@ -0,0 +1,48 @@ + + +
+ {#each sites as site} +
+ + {site.name ?? site.reponame} + + {#if site.description} +

{site.description}

+ {/if} +
+ {#if site.stars} + + + {site.stars.toLocaleString()} + + {/if} + {#if site.repourl} +
+
+ {/each} +
diff --git a/docs/src/routes/docs/showcase/dependency.remote.ts b/docs/src/routes/docs/showcase/dependency.remote.ts new file mode 100644 index 000000000..7bac6fe39 --- /dev/null +++ b/docs/src/routes/docs/showcase/dependency.remote.ts @@ -0,0 +1,206 @@ +import { prerender } from '$app/server'; +import { env } from '$env/dynamic/private'; + +const POPULAR_STAR_THRESHOLD = 100; +const OTHER_STAR_THRESHOLD = 10; + +export type Dependent = { + name?: string; + reponame?: string; + owner?: string; + description: string; + repourl?: string; + homepageurl?: string; + stars?: number; +}; + +export const getDependents = prerender(async () => { + + const featuredSites: Dependent[] = [ + { + name: 'Github Analysis', + description: 'Analyze your GitHub repositories and NPM packages', + repourl: 'https://github.com/techniq/github-analysis', + homepageurl: 'https://github.techniq.dev' + }, + { + name: 'Strava Analysis', + description: 'Analyze your Strava activities', + repourl: 'https://github.com/techniq/strava-analysis', + homepageurl: 'https://strava.techniq.dev' + }, + { + name: 'Zipline AI', + description: 'Features, context and embeddings for real-time AI/ML', + repourl: 'https://zipline.ai/', + homepageurl: 'https://github.com/zipline-ai' + } + ]; + + const supporterSites: Dependent[] = [ + // could this be automated? pull sponsers, check dependents by owner === sponser, and add them here? + { + name: 'Tenzir', + description: 'Open source data pipelines for security teams', + repourl: 'https://github.com/tenzir', + homepageurl: 'https://tenzir.com/' + }, + { + name: 'shadcn-svelte', + description: 'shadcn/ui, but for Svelte.', + repourl: 'https://github.com/huntabyte/shadcn-svelte', + homepageurl: 'https://shadcn-svelte.com/' + }, + { + name: 'Sky Zoo', + description: 'Bluesky stats', + repourl: 'https://skyzoo.blue/', + homepageurl: 'https://github.com/jycouet/jyc.dev' + } + ]; + + // These do not have a GH repo, but will be promoted by adding to the top of popular sites. + const highlightedSites: Dependent[] = [ + { + name: 'GEO audit', + description: 'GEO / AI audit that tracks your visibility impact', + homepageurl: 'https://www.geoaud.it/' + }, + { + name: 'RetireNumber', + description: 'Get a second opinion on your retirement number.', + homepageurl: 'https://retirenumber.com/' + }, + { + name: 'PowerOutage.com', + description: 'Tracks, records, and aggregates power outage data across the World', + homepageurl: 'https://poweroutage.com/' + }, + { + name: 'IOM UN Migration: Ukraine Regional Response', + description: 'Needs, Intentions, and Border Crossings', + homepageurl: + 'https://dtm.iom.int/online-interactive-resources/ukraine-regional-response-dashboard/index.html' + }, + { + name: 'Loyola Chicago: Center for Criminal Justice', + description: 'The First Year of the Pretrial Fairness Act', + homepageurl: 'https://pfa-1yr.loyolaccj.org/' + }, + { + name: 'ftop', + description: 'Comperative performance metrics for Fortnite islands', + homepageurl: 'https://ftop.app/' + }, + { + name: 'Nocturne', + description: 'A next-generation platform for diabetes management', + homepageurl: 'https://nocturne.app/' + } + ]; + + const githubHeaders: Record = { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'LayerChart docs' + }; + + if (env.GITHUB_API_TOKEN) { + const prefix = env.GITHUB_API_TOKEN.startsWith('ghp_') ? 'token' : 'Bearer'; + githubHeaders['Authorization'] = `${prefix} ${env.GITHUB_API_TOKEN}`; + } + + const totalStart = performance.now(); + + // Step 1: Find repos with "layerchart" in package.json via code search + // NOTE: Code search API has a strict rate limit, so pages are fetched sequentially + const repoSet = new Set(); + let page = 1; + const perPage = 100; + + const step1Start = performance.now(); + while (true) { + const searchUrl = `https://api.github.com/search/code?q=${encodeURIComponent('"layerchart" filename:package.json')}&per_page=${perPage}&page=${page}`; + const res = await fetch(searchUrl, { headers: githubHeaders }); + + if (!res.ok) { + console.error(`GitHub code search failed: ${res.status} ${res.statusText}`); + break; + } + + const data = await res.json(); + const items = data.items ?? []; + + for (const item of items) { + const fullName = item.repository?.full_name; + if (fullName && fullName !== 'techniq/layerchart') { + repoSet.add(fullName); + } + } + + if (items.length < perPage || repoSet.size >= data.total_count) break; + page++; + } + console.log( + `[getDependents] Step 1 - Code search: ${((performance.now() - step1Start) / 1000).toFixed(2)}s (${repoSet.size} repos found, ${page} pages)` + ); + + // Step 2: Batch-fetch repo details via GitHub GraphQL (parallel) + const step2Start = performance.now(); + const repos = [...repoSet]; + const batchSize = 50; + + const batchPromises = []; + for (let i = 0; i < repos.length; i += batchSize) { + const batch = repos.slice(i, i + batchSize); + const fragments = batch + .map((fullName, idx) => { + const [owner, name] = fullName.split('/'); + return `repo${idx}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) { stargazerCount description homepageUrl url owner { login } name }`; + }) + .join('\n'); + + batchPromises.push( + fetch('https://api.github.com/graphql', { + method: 'POST', + headers: githubHeaders, + body: JSON.stringify({ query: `{ ${fragments} }` }) + }).then(async (res) => { + if (!res.ok) return []; + const { data } = await res.json(); + if (!data) return []; + return Object.values(data) + .filter(Boolean) + .map((repo: any) => ({ + owner: repo.owner.login, + reponame: repo.name, + description: repo.description || null, + repourl: repo.url, + homepageurl: repo.homepageUrl || null, + stars: repo.stargazerCount + })); + }) + ); + } + + const batchResults = await Promise.all(batchPromises); + const dependents: Dependent[] = batchResults.flat(); + + console.log( + `[getDependents] Step 2 - GraphQL details: ${((performance.now() - step2Start) / 1000).toFixed(2)}s (${dependents.length} repos, ${batchPromises.length} batches)` + ); + console.log(`[getDependents] Total: ${((performance.now() - totalStart) / 1000).toFixed(2)}s`); + + dependents + .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0)) // Sort by stars descending + .filter((d) => featuredSites.some((f) => f.reponame === d.reponame)) // Filter out any featured sites + .filter((d) => supporterSites.some((s) => s.reponame === d.reponame)); // Filter out any supporter sites + const popularSites = [ + ...highlightedSites, + ...dependents.filter((d) => (d.stars ?? 0) >= POPULAR_STAR_THRESHOLD) + ]; + const otherSites = dependents.filter( + (d) => (d.stars ?? 0) >= OTHER_STAR_THRESHOLD && (d.stars ?? 0) < POPULAR_STAR_THRESHOLD + ); + + return { featuredSites, supporterSites, popularSites, otherSites }; +});