diff --git a/app/components/PackageDownloadStats.vue b/app/components/PackageDownloadStats.vue
new file mode 100644
index 0000000000..639a63c816
--- /dev/null
+++ b/app/components/PackageDownloadStats.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+ Weekly Downloads
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/composables/useCharts.ts b/app/composables/useCharts.ts
new file mode 100644
index 0000000000..98ccdf127e
--- /dev/null
+++ b/app/composables/useCharts.ts
@@ -0,0 +1,3 @@
+import { createSharedComposable } from '@vueuse/core'
+
+export const useCharts = createSharedComposable(function useCharts() {})
diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts
index df7affe91e..07d5be42ef 100644
--- a/app/composables/useNpmRegistry.ts
+++ b/app/composables/useNpmRegistry.ts
@@ -141,6 +141,47 @@ export function usePackageDownloads(
)
}
+type NpmDownloadsRangeResponse = {
+ start: string
+ end: string
+ package: string
+ downloads: Array<{ day: string; downloads: number }>
+}
+
+async function fetchNpmDownloadsRange(
+ packageName: string,
+ start: string,
+ end: string,
+): Promise {
+ const encodedName = encodePackageName(packageName)
+ return await $fetch(
+ `${NPM_API}/downloads/range/${start}:${end}/${encodedName}`,
+ )
+}
+
+export function usePackageWeeklyDownloadEvolution(
+ name: MaybeRefOrGetter,
+ options: MaybeRefOrGetter<{
+ weeks?: number
+ endDate?: string
+ }> = {},
+) {
+ return useLazyAsyncData(
+ () => `downloads-weekly-evolution:${toValue(name)}:${JSON.stringify(toValue(options))}`,
+ async () => {
+ const packageName = toValue(name)
+ const { weeks = 12, endDate } = toValue(options) ?? {}
+ const end = endDate ? new Date(`${endDate}T00:00:00.000Z`) : new Date()
+ const start = addDays(end, -(weeks * 7) + 1)
+ const startIso = toIsoDateString(start)
+ const endIso = toIsoDateString(end)
+ const range = await fetchNpmDownloadsRange(packageName, startIso, endIso)
+ const sortedDaily = [...range.downloads].sort((a, b) => a.day.localeCompare(b.day))
+ return buildWeeklyEvolutionFromDaily(sortedDaily)
+ },
+ )
+}
+
const emptySearchResponse = {
objects: [],
total: 0,
diff --git a/app/pages/package/[...name].vue b/app/pages/package/[...name].vue
index d8595a5a97..0704deda7e 100644
--- a/app/pages/package/[...name].vue
+++ b/app/pages/package/[...name].vue
@@ -42,6 +42,7 @@ const orgName = computed(() => {
const { data: pkg, status, error } = usePackage(packageName, requestedVersion)
const { data: downloads } = usePackageDownloads(packageName, 'last-week')
+const { data: weeklyDownloads } = usePackageWeeklyDownloadEvolution(packageName, { weeks: 52 })
// Fetch README for specific version if requested, otherwise latest
const { data: readmeData } = useLazyFetch<{ html: string }>(
@@ -566,6 +567,9 @@ defineOgImageComponent('Package', {
+
+
+
=3.0.1'
+ vue: '>=3.3.0'
+ peerDependenciesMeta:
+ jspdf:
+ optional: true
+
vue-devtools-stub@0.1.0:
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
@@ -14448,6 +14460,10 @@ snapshots:
vue-component-type-helpers@2.2.12: {}
+ vue-data-ui@3.13.0(vue@3.5.27(typescript@5.9.3)):
+ dependencies:
+ vue: 3.5.27(typescript@5.9.3)
+
vue-devtools-stub@0.1.0: {}
vue-flow-layout@0.2.0: {}