Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions apps/website/app/api/npm/downloads/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { NextResponse } from 'next/server';
import type { DownloadPoint, DownloadRange } from '@/lib/types/npm';

const PACKAGE_NAME = 'xmcp';
const NPM_DOWNLOADS_API = 'https://api.npmjs.org/downloads';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const period = searchParams.get('period') || 'last-year';

try {
// Fetch data based on period
let endpoint: string;

// Check if period is a date range (contains ':') or a range keyword
const isRange = period.includes(':') || period === 'last-year';

if (isRange) {
endpoint = `${NPM_DOWNLOADS_API}/range/${period}/${PACKAGE_NAME}`;
} else {
endpoint = `${NPM_DOWNLOADS_API}/point/${period}/${PACKAGE_NAME}`;
}

const response = await fetch(endpoint, {
next: { revalidate: 3600 }, // Cache for 1 hour
});

if (!response.ok) {
throw new Error(`Failed to fetch downloads: ${response.statusText}`);
}

const data: DownloadPoint | DownloadRange = await response.json();

return NextResponse.json(data);
} catch (error) {
console.error('Error fetching download statistics:', error);
return NextResponse.json(
{ error: 'Failed to fetch download statistics' },
{ status: 500 }
);
}
}

// Fetch multiple periods at once
export async function POST(request: Request) {
try {
const { periods } = await request.json();

const promises = periods.map(async (period: string) => {
const isRange = period.includes(':') || period === 'last-year';
const endpoint = isRange
? `${NPM_DOWNLOADS_API}/range/${period}/${PACKAGE_NAME}`
: `${NPM_DOWNLOADS_API}/point/${period}/${PACKAGE_NAME}`;

const response = await fetch(endpoint, {
next: { revalidate: 3600 },
});

if (!response.ok) {
throw new Error(`Failed to fetch ${period}`);
}

return { period, data: await response.json() };
});

const results = await Promise.all(promises);

return NextResponse.json(
results.reduce((acc, { period, data }) => ({
...acc,
[period]: data,
}), {})
);
} catch (error) {
console.error('Error fetching multiple download statistics:', error);
return NextResponse.json(
{ error: 'Failed to fetch download statistics' },
{ status: 500 }
);
}
}
54 changes: 54 additions & 0 deletions apps/website/app/api/npm/metadata/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import type { PackageMetadata } from '@/lib/types/npm';

const PACKAGE_NAME = 'xmcp';
const NPM_REGISTRY_API = 'https://registry.npmjs.org';

export async function GET() {
try {
const response = await fetch(
`${NPM_REGISTRY_API}/${PACKAGE_NAME}`,
{
next: { revalidate: 3600 }, // Cache for 1 hour
}
);

if (!response.ok) {
throw new Error(`Failed to fetch package metadata: ${response.statusText}`);
}

const data: PackageMetadata = await response.json();

// Extract useful summary information
const latestVersion = data['dist-tags']?.latest || '';
const latestVersionData = latestVersion ? data.versions[latestVersion] : null;
const versionsCount = Object.keys(data.versions).length;
const lastPublished = data.time?.[latestVersion] || '';

return NextResponse.json({
name: data.name,
description: data.description,
latestVersion,
versionsCount,
lastPublished,
license: latestVersionData?.license || data.license,
author: data.author,
maintainers: data.maintainers,
keywords: data.keywords,
repository: data.repository,
homepage: data.homepage,
bugs: data.bugs,
unpackedSize: latestVersionData?.dist?.unpackedSize,
fileCount: latestVersionData?.dist?.fileCount,
dependencies: latestVersionData?.dependencies,
devDependencies: latestVersionData?.devDependencies,
peerDependencies: latestVersionData?.peerDependencies,
});
} catch (error) {
console.error('Error fetching package metadata:', error);
return NextResponse.json(
{ error: 'Failed to fetch package metadata' },
{ status: 500 }
);
}
}
39 changes: 39 additions & 0 deletions apps/website/app/api/npm/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import type { SearchResponse } from '@/lib/types/npm';

const PACKAGE_NAME = 'xmcp';
const NPM_SEARCH_API = 'https://registry.npmjs.org/-/v1/search';

export async function GET() {
try {
const response = await fetch(
`${NPM_SEARCH_API}?text=${PACKAGE_NAME}&size=1`,
{
next: { revalidate: 3600 }, // Cache for 1 hour
}
);

if (!response.ok) {
throw new Error(`Failed to fetch search data: ${response.statusText}`);
}

const data: SearchResponse = await response.json();

// Find exact package match
const packageResult = data.objects.find(
(obj) => obj.package.name === PACKAGE_NAME
);

if (!packageResult) {
throw new Error('Package not found in search results');
}

return NextResponse.json(packageResult);
} catch (error) {
console.error('Error fetching search data:', error);
return NextResponse.json(
{ error: 'Failed to fetch search data' },
{ status: 500 }
);
}
}
41 changes: 41 additions & 0 deletions apps/website/app/api/npm/versions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import type { VersionDownloads } from '@/lib/types/npm';

const PACKAGE_NAME = 'xmcp';
const NPM_DOWNLOADS_API = 'https://api.npmjs.org/downloads';

export async function GET() {
try {
const response = await fetch(
`${NPM_DOWNLOADS_API}/point/last-week/${PACKAGE_NAME}`,
{
next: { revalidate: 3600 }, // Cache for 1 hour
}
);

if (!response.ok) {
throw new Error(`Failed to fetch version downloads: ${response.statusText}`);
}

const data: VersionDownloads = await response.json();

// Sort versions by download count (descending)
const sortedDownloads = Object.entries(data.downloads || {})
.sort(([, a], [, b]) => b - a)
.reduce((acc, [version, count]) => ({
...acc,
[version]: count,
}), {});

return NextResponse.json({
...data,
downloads: sortedDownloads,
});
} catch (error) {
console.error('Error fetching version downloads:', error);
return NextResponse.json(
{ error: 'Failed to fetch version downloads' },
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion apps/website/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const revalidate = false;
export default async function sitemap() {
const url = (path: string): string => new URL(path, baseUrl).toString();

const routes = ["", "/docs", "/blog", "/examples", "/x", "/showcase"].map(
const routes = ["", "/docs", "/blog", "/examples", "/x", "/showcase", "/stats"].map(
(route) => ({
url: url(route),
lastModified: new Date().toISOString().split("T")[0],
Expand Down
Loading