From 75739533c400c56aef2b504077ce68f954a0b372 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 2 May 2026 20:19:55 +0100 Subject: [PATCH 01/23] fix: show license from correct version (#2662) --- app/composables/npm/usePackage.ts | 22 +++++++++++----------- shared/types/npm-registry.ts | 4 +++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/composables/npm/usePackage.ts b/app/composables/npm/usePackage.ts index 747de4992e..7864f1b6f5 100644 --- a/app/composables/npm/usePackage.ts +++ b/app/composables/npm/usePackage.ts @@ -16,6 +16,13 @@ function getTrustLevel(version: PackumentVersion): PublishTrustLevel { return 'none' } +function normalizeLicense(license?: PackumentLicense): string | undefined { + if (!license) return undefined + if (typeof license === 'string') return license + if (typeof license.type === 'string') return license.type + return undefined +} + /** * Transform a full Packument into a slimmed version for client-side use. * Reduces payload size by: @@ -72,6 +79,7 @@ export function transformPackument( for (const v of includedVersions) { const version = pkg.versions[v] if (version) { + const versionLicense = normalizeLicense(version.license) if (version.version === requestedVersion) { // Strip readme from each version, extract install scripts info const { readme: _readme, scripts, ...slimVersion } = version @@ -80,25 +88,20 @@ export function transformPackument( const installScripts = scripts ? extractInstallScriptsInfo(scripts) : null versionData = { ...slimVersion, + license: versionLicense, installScripts: installScripts ?? undefined, } } const trustLevel = getTrustLevel(version) const hasProvenance = trustLevel !== 'none' - // Normalize license: some versions use { type: "MIT" } instead of "MIT" - let versionLicense = version.license - if (versionLicense && typeof versionLicense === 'object' && 'type' in versionLicense) { - versionLicense = (versionLicense as { type: string }).type - } - filteredVersions[v] = { hasProvenance, trustLevel, version: version.version, deprecated: version.deprecated, tags: version.tags as string[], - license: typeof versionLicense === 'string' ? versionLicense : undefined, + license: versionLicense, type: typeof version.type === 'string' ? version.type : undefined, } } @@ -113,10 +116,7 @@ export function transformPackument( } // Normalize license field - let license = pkg.license - if (license && typeof license === 'object' && 'type' in license) { - license = license.type - } + const license = normalizeLicense(requestedVersion ? versionData?.license : pkg.license) // Extract storybook field from the requested version (custom package.json field) const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null diff --git a/shared/types/npm-registry.ts b/shared/types/npm-registry.ts index 6ffe94aded..2c568e2503 100644 --- a/shared/types/npm-registry.ts +++ b/shared/types/npm-registry.ts @@ -23,10 +23,12 @@ export interface PackumentVersion extends PackumentVersionWithoutAttestations { dist: PackumentVersionWithoutAttestations['dist'] & { attestations?: NpmVersionAttestations } } +export type PackumentLicense = string | { type: string; url?: string } + export type Packument = Omit & { // Fix for license field being incorrectly typed in @npm/types // TODO: Remove this type override when @npm/types fixes the license field typing - license?: string | { type: string; url?: string } + license?: PackumentLicense versions: Record } From 1cd4f82a788963a8111d4acd3ba67ecdf9b83e3d Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 2 May 2026 20:28:12 +0100 Subject: [PATCH 02/23] feat: add SolidJS data anomalies to download-anomalies.data.ts (#2661) --- app/utils/download-anomalies.data.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/utils/download-anomalies.data.ts b/app/utils/download-anomalies.data.ts index 3cb23fb858..55fdafe989 100644 --- a/app/utils/download-anomalies.data.ts +++ b/app/utils/download-anomalies.data.ts @@ -22,6 +22,11 @@ export const DOWNLOAD_ANOMALIES: DownloadAnomaly[] = [ start: { date: '2023-11-18', weeklyDownloads: 59_611 }, end: { date: '2023-11-21', weeklyDownloads: 150_680 }, }, + { + packageName: 'solid-js', + start: { date: '2024-12-22', weeklyDownloads: 21_395 }, + end: { date: '2024-12-24', weeklyDownloads: 28_308 }, + }, /** * NOTE: * - please add new entries above this comment. From 1fef8da3e8df902d3d992d2151d44f514aed967a Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Sun, 3 May 2026 08:29:06 -0400 Subject: [PATCH 03/23] feat: show badge on top liked packages, link to leaderboard (#2459) --- app/components/Package/Likes.vue | 196 +++--- app/pages/leaderboard/likes.vue | 569 ++++++++++++++++++ i18n/locales/en.json | 16 +- i18n/locales/fr-FR.json | 16 +- i18n/schema.json | 42 ++ nuxt.config.ts | 2 + server/api/leaderboard/likes.get.ts | 10 + server/api/social/likes/[...pkg].get.ts | 12 +- server/utils/atproto/utils/likes.ts | 9 +- server/utils/likes-leaderboard.ts | 253 ++++++++ server/utils/npm-homepage.ts | 111 ++++ shared/types/social.ts | 18 + shared/utils/constants.ts | 4 + shared/utils/fetch-cache-config.ts | 9 +- test/fixtures/likes-leaderboard.ts | 23 + test/fixtures/microlink/kit.svelte.dev.json | 11 + test/fixtures/microlink/nuxt.com.json | 11 + test/fixtures/microlink/react.dev.json | 11 + test/fixtures/microlink/vuejs.org.json | 15 + test/fixtures/mock-routes.cjs | 24 + test/nuxt/a11y.spec.ts | 68 ++- test/nuxt/components/Package/Likes.spec.ts | 117 ++++ test/nuxt/pages/LikesLeaderboardPage.spec.ts | 88 +++ .../server/utils/likes-leaderboard.spec.ts | 295 +++++++++ test/unit/server/utils/npm-homepage.spec.ts | 110 ++++ 25 files changed, 1958 insertions(+), 82 deletions(-) create mode 100644 app/pages/leaderboard/likes.vue create mode 100644 server/api/leaderboard/likes.get.ts create mode 100644 server/utils/likes-leaderboard.ts create mode 100644 server/utils/npm-homepage.ts create mode 100644 test/fixtures/likes-leaderboard.ts create mode 100644 test/fixtures/microlink/kit.svelte.dev.json create mode 100644 test/fixtures/microlink/nuxt.com.json create mode 100644 test/fixtures/microlink/react.dev.json create mode 100644 test/fixtures/microlink/vuejs.org.json create mode 100644 test/nuxt/components/Package/Likes.spec.ts create mode 100644 test/nuxt/pages/LikesLeaderboardPage.spec.ts create mode 100644 test/unit/server/utils/likes-leaderboard.spec.ts create mode 100644 test/unit/server/utils/npm-homepage.spec.ts diff --git a/app/components/Package/Likes.vue b/app/components/Package/Likes.vue index 5c7b68089b..e48f543e62 100644 --- a/app/components/Package/Likes.vue +++ b/app/components/Package/Likes.vue @@ -1,4 +1,5 @@ - diff --git a/app/components/SkeletonBlock.vue b/app/components/SkeletonBlock.vue index 7467e4c103..05ef97ae8c 100644 --- a/app/components/SkeletonBlock.vue +++ b/app/components/SkeletonBlock.vue @@ -1,3 +1,3 @@ diff --git a/app/components/Tab/Item.vue b/app/components/Tab/Item.vue index 6b0f4c3aff..4337c061ce 100644 --- a/app/components/Tab/Item.vue +++ b/app/components/Tab/Item.vue @@ -8,10 +8,12 @@ const props = withDefaults( value: string icon?: IconClass tabId?: string + controlsPanel?: boolean variant?: 'primary' | 'secondary' size?: 'sm' | 'md' }>(), { + controlsPanel: true, variant: 'secondary', size: 'md', }, @@ -22,12 +24,13 @@ const attrs = useAttrs() const selected = inject>('tabs-selected') const getTabId = inject<(value: string) => string>('tabs-tab-id') const getPanelId = inject<(value: string) => string>('tabs-panel-id') + if (!selected || !getTabId || !getPanelId) { throw new Error('TabItem must be used inside a TabRoot component') } const isSelected = computed(() => selected.value === props.value) const resolvedTabId = computed(() => props.tabId ?? getTabId(props.value)) -const resolvedPanelId = computed(() => getPanelId(props.value)) +const resolvedPanelId = computed(() => (props.controlsPanel ? getPanelId(props.value) : undefined)) const select = () => { selected.value = props.value } diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 1fc879a0a4..c5d5734ff6 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -50,6 +50,10 @@ export interface AppSettings { anomaliesFixed: boolean predictionPoints: number } + timelineChart: { + isZeroBased: boolean + showZoom: boolean + } } const DEFAULT_SETTINGS: AppSettings = { @@ -77,6 +81,10 @@ const DEFAULT_SETTINGS: AppSettings = { anomaliesFixed: true, predictionPoints: 4, }, + timelineChart: { + isZeroBased: false, + showZoom: false, + }, } const STORAGE_KEY = 'npmx-settings' diff --git a/app/pages/package-timeline/[[org]]/[packageName].vue b/app/pages/package-timeline/[[org]]/[packageName].vue index bb9f577eb0..a007efde15 100644 --- a/app/pages/package-timeline/[[org]]/[packageName].vue +++ b/app/pages/package-timeline/[[org]]/[packageName].vue @@ -4,6 +4,7 @@ import { compare } from 'semver' import type { TimelineResponse, TimelineVersion, + SubEvent, } from '~~/server/api/registry/timeline/[...pkg].get' import type { TimelineSizeResponse } from '~~/server/api/registry/timeline/sizes/[...pkg].get' @@ -111,14 +112,17 @@ function sizeKey(ver: string) { } async function fetchSizes(offset: number) { + const requestedPackage = packageName.value sizeFetchesInFlight.value++ try { const data = await $fetch( - `/api/registry/timeline/sizes/${packageName.value}`, + `/api/registry/timeline/sizes/${requestedPackage}`, { query: { offset, limit: PAGE_SIZE } }, ) + if (requestedPackage !== packageName.value) return + for (const entry of data.sizes) { - sizeCache.set(sizeKey(entry.version), { + sizeCache.set(`${requestedPackage}@${entry.version}`, { totalSize: entry.totalSize, dependencyCount: entry.dependencyCount, }) @@ -143,13 +147,6 @@ if (import.meta.client) { const bytesFormatter = useBytesFormatter() -interface SubEvent { - key: string - positive: boolean - icon: string - text: string -} - // Detect notable changes between consecutive versions (size, license, ESM, types) // Versions are compared against their semver predecessor, not chronological neighbor, // so interleaved legacy releases don't produce misleading cross-line diffs. @@ -308,6 +305,8 @@ const versionSubEvents = computed(() => { return result }) +const selectedVersion = shallowRef(null) + useSeoMeta({ title: () => `Timeline - ${packageName.value} - npmx`, description: () => `Version timeline for ${packageName.value}`, @@ -325,12 +324,21 @@ useSeoMeta({ page="timeline" /> -
- -
-
+
+
+
+ +
+
+
  1. @@ -346,6 +354,10 @@ useSeoMeta({ class="text-sm font-medium" :class="entry.version === version ? 'text-accent' : ''" dir="ltr" + @mouseenter="selectedVersion = entry.version" + @mouseleave="selectedVersion = null" + @focus="selectedVersion = entry.version" + @blur="selectedVersion = null" > {{ entry.version }} @@ -427,18 +439,3 @@ useSeoMeta({
- - diff --git a/app/utils/charts.ts b/app/utils/charts.ts index 3cdbe3fd34..7c5a86ff3b 100644 --- a/app/utils/charts.ts +++ b/app/utils/charts.ts @@ -9,6 +9,7 @@ import type { VueUiXyDatasetLineItem, } from 'vue-data-ui' import type { ChartTimeGranularity } from '~/types/chart' +import type { SubEvent } from '~~/server/api/registry/timeline/[...pkg].get' export function sum(numbers: number[]): number { return numbers.reduce((a, b) => a + b, 0) @@ -451,6 +452,37 @@ export type FacetBarChartConfig = VueUiHorizontalBarConfig & { $t: TrendTranslateFunction } +export type TimelineSizeCacheValue = { + totalSize: number + dependencyCount: number +} + +export type ConvertedTimelineSizeCacheEntry = TimelineSizeCacheValue & { + name: string +} + +export type EnrichedTimelineSizeCacheEntry = ConvertedTimelineSizeCacheEntry & { + version: string + time?: string + license?: string + type?: string + hasTypes?: boolean + hasTrustedPublisher?: boolean + hasProvenance?: boolean + tags: string[] + events: SubEvent[] + hasPositive: boolean + hasNegative: boolean +} + +export type TimelineChartConfig = VueUiXyConfig & { + metric: 'totalSize' | 'dependencyCount' + packageName: string + copy: (text: string) => Promise + $t: TrendTranslateFunction + numberFormatter: (value: number) => string +} + // Used for TrendsChart.vue export function createAltTextForTrendLineChart({ dataset, @@ -705,6 +737,68 @@ export async function copyAltTextForCompareScatterChart({ await config.copy(altText) } +// Used for TimelineChart.vue +export function createAltTextForTimelineChart({ + dataset, + config, +}: AltCopyArgs) { + if (!dataset) return '' + const metric = + config.metric === 'totalSize' + ? config.$t('package.stats.install_size') + : config.$t('compare.dependencies') + const withEvents = dataset.filter(d => d.events.length) + const first = dataset[0] + const last = dataset.at(-1) + + if (!first || !last) return '' + + const firstValue = config.metric === 'totalSize' ? first?.totalSize : first?.dependencyCount + const lastValue = config.metric === 'totalSize' ? last?.totalSize : last?.dependencyCount + const baseline = firstValue ?? 0 + const current = lastValue ?? baseline + const overall_progress_percentage = + baseline > 0 ? Math.round(((current - baseline) / baseline) * 100) : 0 + + const version_events = withEvents + .map(item => + config.$t('package.timeline.chart.copy_alt.version_events', { + version: item.version, + // eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys + events: item.events.map(e => config.$t(e.text).toLocaleLowerCase()).join(', '), + }), + ) + .join('; ') + + const key_changes = !withEvents.length + ? '' + : config.$t('package.timeline.chart.copy_alt.key_changes', { + version_events, + }) + + const altText = config.$t('package.timeline.chart.copy_alt.general_description', { + metric: metric.toLocaleLowerCase(), + package: config.packageName, + first: first?.version ?? '', + last: last?.version ?? '', + first_value: config.numberFormatter(firstValue ?? 0), + last_value: config.numberFormatter(lastValue ?? 0), + overall_progress_percentage, + key_changes, + watermark: config.$t('package.trends.copy_alt.watermark'), + }) + + return altText +} + +export async function copyAltTextForTimelineChart({ + dataset, + config, +}: AltCopyArgs) { + const altText = createAltTextForTimelineChart({ dataset, config }) + await config.copy(altText) +} + // Used in chart context menu callbacks // @todo replace with downloadFileLink export function loadFile(link: string, filename: string) { diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 9144df697f..5cf61d60a3 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -591,7 +591,18 @@ "trusted_publisher_added": "Trusted publishing enabled", "trusted_publisher_removed": "Trusted publishing removed", "provenance_added": "Provenance enabled", - "provenance_removed": "Provenance removed" + "provenance_removed": "Provenance removed", + "chart": { + "tab_aria_label": "Metric selection", + "base_scale": "start y-axis at zero", + "zoom": "zoom", + "reset_minimap": "reset minimap", + "copy_alt": { + "key_changes": "Key changes: {version_events}.", + "version_events": "version {version}: {events}", + "general_description": "Line chart showing the {metric} of the {package} package, from version {first} to {last}. The {metric} in version {first} is {first_value}, in version {last} is {last_value} ({overall_progress_percentage}% overall). {key_changes} {watermark}." + } + } }, "dependencies": { "title": "Dependency ({count}) | Dependencies ({count})", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 7c21cc4f60..ef6b16f298 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -579,7 +579,18 @@ "trusted_publisher_added": "Vérification ajoutée", "trusted_publisher_removed": "Vérification enlevée", "provenance_added": "Preuve de provenance ajoutée", - "provenance_removed": "Preuve de provenance enlevée" + "provenance_removed": "Preuve de provenance enlevée", + "chart": { + "tab_aria_label": "Sélection de métrique", + "base_scale": "positionner les ordonnées à zéro", + "zoom": "zoom", + "reset_minimap": "Réinitialiser la mini-carte", + "copy_alt": { + "key_changes": "Principaux changements: {version_events}", + "version_events": "version {version}: {events}", + "general_description": "Graphique en ligne montrant la métrique {metric} pour le paquet {package}, depuis la version {first} à {last}. La valeur de la métrique {metric} pour la version {first} est {first_value}, et {last_value} pour la version {last} ({overall_progress_percentage}% dans l'ensemble). {key_changes} {watermark}." + } + } }, "dependencies": { "title": "Dépendances ({count})", diff --git a/i18n/schema.json b/i18n/schema.json index b83d9682c4..31b662ee58 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1779,6 +1779,39 @@ }, "provenance_removed": { "type": "string" + }, + "chart": { + "type": "object", + "properties": { + "tab_aria_label": { + "type": "string" + }, + "base_scale": { + "type": "string" + }, + "zoom": { + "type": "string" + }, + "reset_minimap": { + "type": "string" + }, + "copy_alt": { + "type": "object", + "properties": { + "key_changes": { + "type": "string" + }, + "version_events": { + "type": "string" + }, + "general_description": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/package.json b/package.json index abb4fe1a0b..5d4e8c9427 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "vite-plugin-pwa": "1.2.0", "vite-plus": "0.1.16", "vue": "3.5.34", - "vue-data-ui": "3.18.2", + "vue-data-ui": "3.19.3", "vue-router": "5.0.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fff23140e..07ce4e5649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,8 +254,8 @@ importers: specifier: 3.5.34 version: 3.5.34(typescript@6.0.2) vue-data-ui: - specifier: 3.18.2 - version: 3.18.2(vue@3.5.34) + specifier: 3.19.3 + version: 3.19.3(vue@3.5.34) vue-router: specifier: 5.0.4 version: 5.0.4(@vue/compiler-sfc@3.5.34)(vue@3.5.34) @@ -4009,61 +4009,31 @@ packages: cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-arm64@0.22.0': - resolution: {integrity: sha512-/exgXceakHbQrzaHTtKOe7MuDATaWMCCWpsCDQCZKeYhLGXzComipTrCYnHzAXrdnNBb5r5K+RRf5A6ormrhMA==} - cpu: [arm64] - os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.20.0': resolution: {integrity: sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.22.0': - resolution: {integrity: sha512-xFGdIahlmUbK+/MpZ5y08D0ewMGLDbd2Vki5wxVFYg50lSrtgPAtdDl+kqKZLNaFu0zpMar8n9wv1le05sL/jw==} - cpu: [x64] - os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.20.0': resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-arm64@0.22.0': - resolution: {integrity: sha512-53RvC9f77eUo+V1dfQNwGVnsIfPJFMibRR0ee128EUpYNDOZe/ojmCfuXJeU7cY91V7r7fZSm42KPJocXUX8og==} - cpu: [arm64] - os: [linux] - '@oxlint-tsgolint/linux-x64@0.20.0': resolution: {integrity: sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.22.0': - resolution: {integrity: sha512-evZcJAZ9hjNyuN69RnXwbt+U2pAOcYt+yvqukgugiCkRm4iBZ0R0CvpY1tgfG2XcGUhEPh8dljO+nPZTEVGpCQ==} - cpu: [x64] - os: [linux] - '@oxlint-tsgolint/win32-arm64@0.20.0': resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-arm64@0.22.0': - resolution: {integrity: sha512-7jTO+k1mr5BxRAI2fxc1NRcE3MAbHNZ0Vef9SD1yAR6d1E6qEv5D/D7yuHpQpw6AO3qoecSVo2Jzr+JirN61+w==} - cpu: [arm64] - os: [win32] - '@oxlint-tsgolint/win32-x64@0.20.0': resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==} cpu: [x64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.22.0': - resolution: {integrity: sha512-7lbl9XFcqO+scsynxMzTQdl0XUe6sBUCyY/oGWvCB+JmV4U+70vzSyZJdTEzzxtkZiNnUVFFh9RJLmoiQSne+w==} - cpu: [x64] - os: [win32] - '@oxlint/binding-android-arm-eabi@1.58.0': resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9442,10 +9412,6 @@ packages: resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} hasBin: true - oxlint-tsgolint@0.22.0: - resolution: {integrity: sha512-ku4MecLmCQIj1ScCtzNAqTuyl0BJQ02B36fJT+c5XQihHpYSFak+FC3GYO5fPyYk4oDwi0w0S7hTvrpNzuZhig==} - hasBin: true - oxlint@1.58.0: resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -11522,8 +11488,8 @@ packages: vue-component-type-helpers@3.2.8: resolution: {integrity: sha512-9689efAXhN/EV86plgkL/XFiJSXhGtWPG6JDboZ+QnjlUWUUQrQ0ILKQtw4iQsuwIwu5k6Aw+JnehDe7161e7A==} - vue-data-ui@3.18.2: - resolution: {integrity: sha512-BJP+YMrJeAdVnT2rmBsZBe+rHksReCHrzFM8MYXAgndgAdPJlzsLigylwflLhm9sndQeAt6ihCslX0VIU+nyUQ==} + vue-data-ui@3.19.3: + resolution: {integrity: sha512-cBBD6NnZnXMQ1ZDKNxhjPqxq7bxwzZL+WnNxM8O2lJ74384TzfbdMKiKk94QH6jy7B4odCl0MCt4VRgp1LDYCA==} peerDependencies: jspdf: '>=3.0.1' vue: '>=3.3.0' @@ -15579,39 +15545,21 @@ snapshots: '@oxlint-tsgolint/darwin-arm64@0.20.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.22.0': - optional: true - '@oxlint-tsgolint/darwin-x64@0.20.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.22.0': - optional: true - '@oxlint-tsgolint/linux-arm64@0.20.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.22.0': - optional: true - '@oxlint-tsgolint/linux-x64@0.20.0': optional: true - '@oxlint-tsgolint/linux-x64@0.22.0': - optional: true - '@oxlint-tsgolint/win32-arm64@0.20.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.22.0': - optional: true - '@oxlint-tsgolint/win32-x64@0.20.0': optional: true - '@oxlint-tsgolint/win32-x64@0.22.0': - optional: true - '@oxlint/binding-android-arm-eabi@1.58.0': optional: true @@ -21924,16 +21872,6 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.20.0 '@oxlint-tsgolint/win32-x64': 0.20.0 - oxlint-tsgolint@0.22.0: - optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.22.0 - '@oxlint-tsgolint/darwin-x64': 0.22.0 - '@oxlint-tsgolint/linux-arm64': 0.22.0 - '@oxlint-tsgolint/linux-x64': 0.22.0 - '@oxlint-tsgolint/win32-arm64': 0.22.0 - '@oxlint-tsgolint/win32-x64': 0.22.0 - optional: true - oxlint@1.58.0(oxlint-tsgolint@0.20.0): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.58.0 @@ -21981,30 +21919,6 @@ snapshots: oxlint-tsgolint: 0.20.0 optional: true - oxlint@1.61.0(oxlint-tsgolint@0.22.0): - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.61.0 - '@oxlint/binding-android-arm64': 1.61.0 - '@oxlint/binding-darwin-arm64': 1.61.0 - '@oxlint/binding-darwin-x64': 1.61.0 - '@oxlint/binding-freebsd-x64': 1.61.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.61.0 - '@oxlint/binding-linux-arm-musleabihf': 1.61.0 - '@oxlint/binding-linux-arm64-gnu': 1.61.0 - '@oxlint/binding-linux-arm64-musl': 1.61.0 - '@oxlint/binding-linux-ppc64-gnu': 1.61.0 - '@oxlint/binding-linux-riscv64-gnu': 1.61.0 - '@oxlint/binding-linux-riscv64-musl': 1.61.0 - '@oxlint/binding-linux-s390x-gnu': 1.61.0 - '@oxlint/binding-linux-x64-gnu': 1.61.0 - '@oxlint/binding-linux-x64-musl': 1.61.0 - '@oxlint/binding-openharmony-arm64': 1.61.0 - '@oxlint/binding-win32-arm64-msvc': 1.61.0 - '@oxlint/binding-win32-ia32-msvc': 1.61.0 - '@oxlint/binding-win32-x64-msvc': 1.61.0 - oxlint-tsgolint: 0.22.0 - optional: true - p-all@5.0.1: dependencies: p-map: 6.0.0 @@ -24263,7 +24177,7 @@ snapshots: optionalDependencies: eslint: 9.39.2(jiti@2.6.1) optionator: 0.9.4 - oxlint: 1.61.0(oxlint-tsgolint@0.22.0) + oxlint: 1.61.0(oxlint-tsgolint@0.20.0) typescript: 6.0.2 vue-tsc: 3.2.6(typescript@6.0.2) @@ -24419,7 +24333,7 @@ snapshots: vue-component-type-helpers@3.2.8: {} - vue-data-ui@3.18.2(vue@3.5.34): + vue-data-ui@3.19.3(vue@3.5.34): dependencies: vue: 3.5.34(typescript@6.0.2) diff --git a/server/api/registry/timeline/[...pkg].get.ts b/server/api/registry/timeline/[...pkg].get.ts index 05ffbecdf6..aa4455513f 100644 --- a/server/api/registry/timeline/[...pkg].get.ts +++ b/server/api/registry/timeline/[...pkg].get.ts @@ -18,6 +18,13 @@ export interface TimelineResponse { total: number } +export interface SubEvent { + key: string + positive: boolean + icon: string + text: string +} + /** * Returns paginated version timeline data for a package. * diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index c4381fd7b6..2fae464f53 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -269,6 +269,7 @@ import SearchProviderToggleServer from '~/components/SearchProviderToggle.server import PackageTrendsChart from '~/components/Package/TrendsChart.vue' import FacetBarChart from '~/components/Compare/FacetBarChart.vue' import FacetScatterChart from '~/components/Compare/FacetScatterChart.vue' +import PackageTimelineChart from '~/components/Package/TimelineChart.vue' import PackageLikeCard from '~/components/Package/LikeCard.vue' import SizeIncrease from '~/components/Package/SizeIncrease.vue' import SizeDecrease from '~/components/Package/SizeDecrease.vue' @@ -1066,6 +1067,22 @@ describe('component accessibility audits', () => { expect(results.violations).toEqual([]) }) + describe('PackageTimelineChart', () => { + it('should have no accessibility violations', async () => { + const wrapper = await mountSuspended(PackageTimelineChart, { + props: { + sizeCache: new Map(), + versionSubEvents: new Map(), + timelineEntries: [], + selectedVersion: null, + loading: false, + }, + }) + const results = await runAxe(wrapper) + expect(results.violations).toEqual([]) + }) + }) + describe('FacetBarChart', () => { it('should have no accessibility violations', async () => { const wrapper = await mountSuspended(FacetBarChart, { diff --git a/test/unit/app/utils/charts.spec.ts b/test/unit/app/utils/charts.spec.ts index 72c3d6c998..ae2ff47593 100644 --- a/test/unit/app/utils/charts.spec.ts +++ b/test/unit/app/utils/charts.spec.ts @@ -11,6 +11,8 @@ import { copyAltTextForTrendLineChart, createAltTextForVersionsBarChart, copyAltTextForVersionsBarChart, + createAltTextForTimelineChart, + copyAltTextForTimelineChart, loadFile, sanitise, insertLineBreaks, @@ -19,6 +21,8 @@ import { type TrendLineDataset, type VersionsBarConfig, type VersionsBarDataset, + type TimelineChartConfig, + type EnrichedTimelineSizeCacheEntry, } from '~/utils/charts' import type { AltCopyArgs } from 'vue-data-ui' @@ -35,6 +39,19 @@ function createTranslateMock() { return { translate, calls } } +function createTimelineConfig(overrides: Partial = {}): TimelineChartConfig { + const { translate } = createTranslateMock() + const config: TimelineChartConfig = { + numberFormatter: (value: number) => `nf${value}`, + packageName: 'nuxt', + metric: 'totalSize', + copy: vi.fn(async () => undefined), + $t: translate, + } as unknown as TimelineChartConfig + + return { ...config, ...overrides } +} + function createTrendLineConfig(overrides: Partial = {}): TrendLineConfig { const { translate } = createTranslateMock() @@ -1187,6 +1204,80 @@ describe('copyAltTextForVersionsBarChart', () => { }) }) +const timelineDataset = [ + { + dependencyCount: 100, + events: [], + version: '4.0.0', + totalSize: 120_000_000, + }, + { + dependencyCount: 80, + events: [], + version: '4.0.1', + totalSize: 115_000_000, + }, +] as unknown as EnrichedTimelineSizeCacheEntry[] + +describe('createAltTextForTimelineChart', () => { + it('handles empty dataset without throwing', () => { + const { translate } = createTranslateMock() + const config = createTimelineConfig({ $t: translate }) + + expect(() => + createAltTextForTimelineChart({ + dataset: [], + config, + } as AltCopyArgs), + ).not.toThrow() + }) + + it('returns empty string when dataset is null', () => { + const translateMock = createTranslateMock() + const config = createTimelineConfig({ $t: translateMock.translate }) + + const result = createAltTextForTimelineChart({ + dataset: null, + config, + } as unknown as AltCopyArgs) + + expect(result).toBe('') + expect(translateMock.calls).toHaveLength(0) + }) + + it('returns an alt text', () => { + const translateMock = createTranslateMock() + const config = createTimelineConfig({ $t: translateMock.translate }) + + const result = createAltTextForTimelineChart({ + dataset: timelineDataset, + config, + } as unknown as AltCopyArgs) + + expect(result).toBe('t:package.timeline.chart.copy_alt.general_description') + expect(translateMock.calls).toHaveLength(3) + }) +}) + +describe('copyAltTextForTimelineChart', () => { + it('forwards createAltTextForTimelineChart result to config.copy', async () => { + const copyMock = vi.fn(async () => undefined) + const config = createTimelineConfig({ copy: copyMock }) + const expected = createAltTextForTimelineChart({ + dataset: timelineDataset, + config, + }) + + await copyAltTextForTimelineChart({ + dataset: timelineDataset, + config, + } as AltCopyArgs) + + expect(copyMock).toHaveBeenCalledTimes(1) + expect(copyMock).toHaveBeenCalledWith(expected) + }) +}) + describe('loadFile', () => { let createElementMock: ReturnType let clickMock: ReturnType From 8936f1a60ebcfc5c12ed1393ddec0864f936769a Mon Sep 17 00:00:00 2001 From: Alec Lloyd Probert <55991794+graphieros@users.noreply.github.com> Date: Fri, 8 May 2026 16:56:09 +0200 Subject: [PATCH 17/23] fix: move tooltips to the sides on line charts to free the view (#2688) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- app/components/Package/TimelineChart.vue | 5 + app/components/Package/TrendsChart.vue | 9 ++ app/composables/useChartTooltipPosition.ts | 26 ++++ package.json | 2 +- pnpm-lock.yaml | 10 +- .../use-chart-tooltip-position.spec.ts | 122 ++++++++++++++++++ 6 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 app/composables/useChartTooltipPosition.ts create mode 100644 test/unit/app/composables/use-chart-tooltip-position.spec.ts diff --git a/app/components/Package/TimelineChart.vue b/app/components/Package/TimelineChart.vue index ac2b9de80d..53b0be9727 100644 --- a/app/components/Package/TimelineChart.vue +++ b/app/components/Package/TimelineChart.vue @@ -18,6 +18,7 @@ import { } from '~/utils/charts' import type { TimelineVersion, SubEvent } from '~~/server/api/registry/timeline/[...pkg].get' import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark' +import { useChartTooltipPosition } from '~/composables/useChartTooltipPosition' import('vue-data-ui/style.css') @@ -250,6 +251,8 @@ function buildExportFilename(extension: 'png' | 'csv' | 'svg') { return `${sanitise(packageName.value)}_${$t('package.links.timeline')}_${metricLabel.value.toLocaleLowerCase().replaceAll(' ', '-')}.${extension}` } +const tooltipPosition = useChartTooltipPosition(chartRef) + const config = computed(() => { return { theme: isDarkMode.value ? 'dark' : '', @@ -316,6 +319,8 @@ const config = computed(() => { color: colors.value.fg, }, tooltip: { + position: tooltipPosition.value, + offsetX: 24, borderColor: colors.value.border, borderRadius: 6, backgroundColor: colors.value.bg, diff --git a/app/components/Package/TrendsChart.vue b/app/components/Package/TrendsChart.vue index bf57c7c760..f9ca4fb701 100644 --- a/app/components/Package/TrendsChart.vue +++ b/app/components/Package/TrendsChart.vue @@ -25,6 +25,7 @@ import { } from '~/utils/chart-data-prediction' import { applyBlocklistCorrection, getAnomaliesForPackages } from '~/utils/download-anomalies' import { copyAltTextForTrendLineChart, sanitise, loadFile, applyEllipsis } from '~/utils/charts' +import { useChartTooltipPosition } from '~/composables/useChartTooltipPosition' import('vue-data-ui/style.css') @@ -67,6 +68,8 @@ const resolvedMode = shallowRef<'light' | 'dark'>('light') const rootEl = shallowRef(null) const isZoomed = shallowRef(false) +const chartRef = useTemplateRef('chartRef') + function setIsZoom({ isZoom }: { isZoom: boolean }) { isZoomed.value = isZoom } @@ -1385,6 +1388,8 @@ watch( { immediate: true }, ) +const tooltipPosition = useChartTooltipPosition(chartRef) + // VueUiXy chart component configuration const chartConfig = computed(() => { return { @@ -1518,6 +1523,9 @@ const chartConfig = computed(() => { legend: { show: false, position: 'top' }, tooltip: { teleportTo: props.inModal ? '#chart-modal' : undefined, + position: tooltipPosition.value, + offsetX: 24, + offsetY: isMultiPackageMode.value ? undefined : -24, borderColor: 'transparent', backdropFilter: false, backgroundColor: 'transparent', @@ -1930,6 +1938,7 @@ const isSparklineLayout = computed({ :aria-labelledby="isMultiPackageMode ? 'combined-chart-layout-tab' : undefined" > =3.0.1' vue: '>=3.3.0' @@ -24333,7 +24333,7 @@ snapshots: vue-component-type-helpers@3.2.8: {} - vue-data-ui@3.19.3(vue@3.5.34): + vue-data-ui@3.19.4(vue@3.5.34): dependencies: vue: 3.5.34(typescript@6.0.2) diff --git a/test/unit/app/composables/use-chart-tooltip-position.spec.ts b/test/unit/app/composables/use-chart-tooltip-position.spec.ts new file mode 100644 index 0000000000..70429da797 --- /dev/null +++ b/test/unit/app/composables/use-chart-tooltip-position.spec.ts @@ -0,0 +1,122 @@ +import type { computed } from 'vue' +import { ref, shallowRef } from 'vue' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useChartTooltipPosition } from '~/composables/useChartTooltipPosition' + +const mouseState = vi.hoisted(() => ({ + target: null as unknown, +})) + +const elementX = ref(0) +const elementWidth = ref(0) +const isOutside = ref(true) + +const MockHTMLElement = class { + public readonly nodeType = 1 +} + +vi.stubGlobal('HTMLElement', MockHTMLElement) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +vi.mock('@vueuse/core', () => ({ + useMouseInElement: vi.fn(target => { + mouseState.target = target + + return { + elementX, + elementWidth, + isOutside, + } + }), +})) + +describe('useChartTooltipPosition', () => { + beforeEach(() => { + vi.stubGlobal('HTMLElement', MockHTMLElement) + elementX.value = 0 + elementWidth.value = 0 + isOutside.value = true + mouseState.target = null + }) + + it('returns center when the mouse is outside', () => { + const element = new MockHTMLElement() as HTMLElement + const position = useChartTooltipPosition(shallowRef(element)) + + isOutside.value = true + elementWidth.value = 100 + elementX.value = 75 + + expect(position.value).toBe('center') + }) + + it('returns center when element width is 0', () => { + const element = new MockHTMLElement() as HTMLElement + const position = useChartTooltipPosition(shallowRef(element)) + isOutside.value = false + elementWidth.value = 0 + elementX.value = 75 + expect(position.value).toBe('center') + }) + + it('returns left when the mouse is on the right half of the element', () => { + const element = new MockHTMLElement() as HTMLElement + const position = useChartTooltipPosition(shallowRef(element)) + isOutside.value = false + elementWidth.value = 100 + elementX.value = 51 + expect(position.value).toBe('left') + }) + + it('returns right when the mouse is on the left half of the element', () => { + const element = new MockHTMLElement() as HTMLElement + const position = useChartTooltipPosition(shallowRef(element)) + isOutside.value = false + elementWidth.value = 100 + elementX.value = 49 + expect(position.value).toBe('right') + }) + + it('returns right when the mouse is exactly at the center', () => { + const element = new MockHTMLElement() as HTMLElement + const position = useChartTooltipPosition(shallowRef(element)) + isOutside.value = false + elementWidth.value = 100 + elementX.value = 50 + expect(position.value).toBe('right') + }) + + it('accepts a Vue component ref exposing $el', () => { + const element = new MockHTMLElement() as HTMLElement + const componentReference = shallowRef({ $el: element }) + useChartTooltipPosition(componentReference) + expect((mouseState.target as ReturnType).value).toBe(element) + }) + + it('returns null as target when ref value is null', () => { + useChartTooltipPosition(shallowRef(null)) + expect((mouseState.target as ReturnType).value).toBe(null) + }) + + it('returns null when component ref has no $el', () => { + const componentReference = shallowRef({}) + useChartTooltipPosition(componentReference) + expect((mouseState.target as ReturnType).value).toBe(null) + }) + + it('uses the HTMLElement directly as target', () => { + const element = new MockHTMLElement() as HTMLElement + useChartTooltipPosition(shallowRef(element)) + expect((mouseState.target as ReturnType).value).toBe(element) + }) + + it('uses the component $el as target', () => { + const element = new MockHTMLElement() as HTMLElement + const componentReference = shallowRef({ $el: element }) + useChartTooltipPosition(componentReference) + expect((mouseState.target as ReturnType).value).toBe(element) + }) +}) From 8ef7525a5263f1b2ca0bab1efb8bcf5514db4d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palmeiro?= Date: Fri, 8 May 2026 16:06:00 +0100 Subject: [PATCH 18/23] fix(i18n): translate missing pt-PT strings for leaderboard and likes (#2682) --- i18n/locales/pt-PT.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/i18n/locales/pt-PT.json b/i18n/locales/pt-PT.json index 575c4a803d..8540dec556 100644 --- a/i18n/locales/pt-PT.json +++ b/i18n/locales/pt-PT.json @@ -317,6 +317,7 @@ "warnings": "Avisos:", "go_back_home": "Voltar para a página inicial", "per_week": "/ semana", + "per_week_short": "/sem", "vanity_downloads_hint": "Número de vaidade: nenhum pacote apresentado | Número de vaidade: para o pacote apresentado | Número de vaidade: Soma de {count} pacotes apresentados", "sort": { "name": "nome", @@ -451,7 +452,10 @@ }, "likes": { "like": "Gostar deste pacote", - "unlike": "Remover gosto deste pacote" + "unlike": "Remover gosto deste pacote", + "top_rank_tooltip": "Este está entre os 10 pacotes com mais gostos no npmx! (#{rank})", + "top_rank_label": "#{rank}", + "top_rank_link_label": "Ver tabela de classificação de gostos. Este pacote está em #{rank}." }, "docs": { "contents": "Conteúdo", @@ -791,6 +795,16 @@ "tarball": "Transferir Tarball como .tar.gz" } }, + "leaderboard": { + "likes": { + "title": "Tabela de Classificação de Gostos", + "description": "Os 10 pacotes com mais gostos no npmx neste momento.", + "rank": "Classificação", + "likes": "Gostos", + "unavailable_title": "Ainda não há tabela de classificação de gostos", + "unavailable_description": "Não temos uma tabela de classificação de gostos para mostrar neste momento." + } + }, "connector": { "modal": { "title": "Conector Local", From 8d4e18db866ec4053fd41139f741780a34b92494 Mon Sep 17 00:00:00 2001 From: abeer0 <47961062+iiio2@users.noreply.github.com> Date: Fri, 8 May 2026 21:24:24 +0600 Subject: [PATCH 19/23] chore: explicitly import node process (#2685) --- lunaria/lunaria.ts | 1 + modules/build-env.ts | 1 + modules/security-headers.ts | 1 + scripts/find-invalid-translations.ts | 1 + scripts/generate-file-tree-sprite.ts | 1 + scripts/next-version.ts | 1 + scripts/remove-unused-translations.ts | 1 + scripts/unocss-checker.ts | 1 + test/unit/modules/security-headers.spec.ts | 1 + uno.config.ts | 1 + 10 files changed, 10 insertions(+) diff --git a/lunaria/lunaria.ts b/lunaria/lunaria.ts index 249d364461..c173aead38 100644 --- a/lunaria/lunaria.ts +++ b/lunaria/lunaria.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import { createLunaria } from '@lunariajs/core' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { Page } from './components.ts' diff --git a/modules/build-env.ts b/modules/build-env.ts index ab3714d90a..7097902bd2 100644 --- a/modules/build-env.ts +++ b/modules/build-env.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import type { BuildInfo, EnvType } from '../shared/types' import { createResolver, defineNuxtModule } from 'nuxt/kit' import { isCI } from 'std-env' diff --git a/modules/security-headers.ts b/modules/security-headers.ts index f32c838c48..8c811446b1 100644 --- a/modules/security-headers.ts +++ b/modules/security-headers.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import { defineNuxtModule, useNuxt } from 'nuxt/kit' import { BLUESKY_API } from '#shared/utils/constants' import { ALL_KNOWN_GIT_API_ORIGINS } from '#shared/utils/git-providers' diff --git a/scripts/find-invalid-translations.ts b/scripts/find-invalid-translations.ts index 173ae7a2b7..9f70e01d38 100644 --- a/scripts/find-invalid-translations.ts +++ b/scripts/find-invalid-translations.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import process from 'node:process' import { join } from 'node:path' import { createI18NReport, type I18NItem } from 'vue-i18n-extract' import { colors } from './utils/colors.ts' diff --git a/scripts/generate-file-tree-sprite.ts b/scripts/generate-file-tree-sprite.ts index 2ebad92ab2..ac691de737 100644 --- a/scripts/generate-file-tree-sprite.ts +++ b/scripts/generate-file-tree-sprite.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import type { IconifyJSON } from '@iconify-json/lucide' import { promises as fs } from 'node:fs' import { fileURLToPath } from 'node:url' diff --git a/scripts/next-version.ts b/scripts/next-version.ts index 9e9f7999a2..4713a071c0 100644 --- a/scripts/next-version.ts +++ b/scripts/next-version.ts @@ -22,6 +22,7 @@ */ import { execFileSync } from 'node:child_process' +import process from 'node:process' function git(...args: string[]): string { return execFileSync('git', args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() diff --git a/scripts/remove-unused-translations.ts b/scripts/remove-unused-translations.ts index 602d9643d2..b0e6edcc75 100644 --- a/scripts/remove-unused-translations.ts +++ b/scripts/remove-unused-translations.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import process from 'node:process' import { join } from 'node:path' import { createI18NReport, type I18NItem } from 'vue-i18n-extract' import { colors } from './utils/colors.ts' diff --git a/scripts/unocss-checker.ts b/scripts/unocss-checker.ts index 0cf58cd8af..4a80ea1e6f 100644 --- a/scripts/unocss-checker.ts +++ b/scripts/unocss-checker.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import type { Dirent } from 'node:fs' import { glob, readFile } from 'node:fs/promises' import { resolve } from 'node:path' diff --git a/test/unit/modules/security-headers.spec.ts b/test/unit/modules/security-headers.spec.ts index addf91a106..f8b285318c 100644 --- a/test/unit/modules/security-headers.spec.ts +++ b/test/unit/modules/security-headers.spec.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import { beforeEach, describe, expect, it, vi } from 'vitest' const { useNuxt } = vi.hoisted(() => ({ diff --git a/uno.config.ts b/uno.config.ts index eef2b11cb3..ddbbafb7a0 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,3 +1,4 @@ +import process from 'node:process' import { defineConfig, presetIcons, From f8cff4ce0c29ef961af42c201846f46f653a0c66 Mon Sep 17 00:00:00 2001 From: Alec Lloyd Probert <55991794+graphieros@users.noreply.github.com> Date: Fri, 8 May 2026 18:27:21 +0200 Subject: [PATCH 20/23] fix: persist zoom state in line charts (#2689) --- app/components/Package/TrendsChart.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/Package/TrendsChart.vue b/app/components/Package/TrendsChart.vue index f9ca4fb701..6499064a5a 100644 --- a/app/components/Package/TrendsChart.vue +++ b/app/components/Package/TrendsChart.vue @@ -1589,6 +1589,7 @@ const chartConfig = computed(() => { maxWidth: isMobile.value ? 350 : 500, highlightColor: colors.value.bgElevated, useResetSlot: true, + keepState: true, minimap: { show: true, lineColor: '#FAFAFA', From 3cb1e1c36f97a77b12a1a77c3e9c0219af9a16a5 Mon Sep 17 00:00:00 2001 From: "Adebesin Tolulope (Lope)" Date: Sat, 9 May 2026 02:03:35 +0100 Subject: [PATCH 21/23] feat: restore brand link in footer and command palette (#2692) --- app/components/AppFooter.vue | 4 ++++ app/composables/useCommandPaletteGlobalCommands.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index 6723549fd6..60c6259aea 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -48,6 +48,10 @@ const footerSections: Array<{ label: string; links: FooterLink[] }> = [ name: t('footer.about'), href: '/about', }, + { + name: t('footer.brand'), + href: '/brand', + }, { name: t('a11y.footer_title'), href: '/accessibility', diff --git a/app/composables/useCommandPaletteGlobalCommands.ts b/app/composables/useCommandPaletteGlobalCommands.ts index 199f411884..e5013a466c 100644 --- a/app/composables/useCommandPaletteGlobalCommands.ts +++ b/app/composables/useCommandPaletteGlobalCommands.ts @@ -310,6 +310,16 @@ export function useCommandPaletteGlobalCommands() { ), to: { name: 'blog' }, }, + { + id: 'brand', + group: 'npmx', + label: t('footer.brand'), + keywords: [t('footer.brand')], + iconClass: 'i-lucide:palette', + active: route.name === 'brand', + activeLabel: activeLabel(route.name === 'brand', t('command_palette.here')), + to: { name: 'brand' }, + }, { id: 'privacy', group: 'npmx', From 49eb15f464d90294d775d511b2b7893cba992eb5 Mon Sep 17 00:00:00 2001 From: JounQin Date: Sat, 9 May 2026 09:04:22 +0800 Subject: [PATCH 22/23] chore: increase timeout for connection requests to 30 seconds (#2684) Co-authored-by: Philippe Serhal --- app/composables/useConnector.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/composables/useConnector.ts b/app/composables/useConnector.ts index 4dbd2d984d..dee65f413d 100644 --- a/app/composables/useConnector.ts +++ b/app/composables/useConnector.ts @@ -55,6 +55,7 @@ interface StateResponse { const STORAGE_KEY = 'npmx-connector' const DEFAULT_PORT = 31415 +const CONNECT_TIMEOUT_MS = 30000 export const useConnector = createSharedComposable(function useConnector() { const { settings } = useSettings() @@ -115,7 +116,7 @@ export const useConnector = createSharedComposable(function useConnector() { const response = await $fetch(`http://127.0.0.1:${port}/connect`, { method: 'POST', body: { token }, - timeout: 5000, + timeout: CONNECT_TIMEOUT_MS, }) if (response.success && response.data) { @@ -180,7 +181,7 @@ export const useConnector = createSharedComposable(function useConnector() { headers: { Authorization: `Bearer ${config.value.token}`, }, - timeout: 5000, + timeout: CONNECT_TIMEOUT_MS, }) if (response.success && response.data) { From 07a192ca1929193e8472941840d50bf922b166aa Mon Sep 17 00:00:00 2001 From: "Adebesin Tolulope (Lope)" Date: Sat, 9 May 2026 02:04:33 +0100 Subject: [PATCH 23/23] fix: remove duplicate og:image meta tag on package pages (#2693) --- .../[packageName]/v/[version]/[...filePath].vue | 10 +--------- app/pages/package-docs/[...path].vue | 10 +--------- app/pages/package/[[org]]/[name].vue | 10 +--------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue b/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue index 8c2e43988d..340836bad8 100644 --- a/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue +++ b/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue @@ -324,15 +324,7 @@ defineOgImage( version: () => version.value, variant: 'code-tree', }, - [ - { key: 'og', alt: () => `Source code file tree for ${packageName.value}@${version.value}` }, - { - key: 'whatsapp', - width: 800, - height: 800, - alt: () => `Source code file tree for ${packageName.value}@${version.value}`, - }, - ], + { alt: () => `Source code file tree for ${packageName.value}@${version.value}` }, ) useCommandPaletteContextCommands( diff --git a/app/pages/package-docs/[...path].vue b/app/pages/package-docs/[...path].vue index d07a3d023d..a7f44157ef 100644 --- a/app/pages/package-docs/[...path].vue +++ b/app/pages/package-docs/[...path].vue @@ -149,15 +149,7 @@ defineOgImage( version: () => resolvedVersion.value, variant: 'function-tree', }, - [ - { key: 'og', alt: () => `API documentation for ${packageName.value}` }, - { - key: 'whatsapp', - width: 800, - height: 800, - alt: () => `API documentation for ${packageName.value}`, - }, - ], + { alt: () => `API documentation for ${packageName.value}` }, ) const showLoading = computed( diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 1ba0190fc7..c709e2f5d4 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -42,15 +42,7 @@ defineOgImage( version: () => requestedVersion.value, variant: 'download-chart', }, - [ - { key: 'og', alt: () => `npm package ${packageName.value} download chart and stats` }, - { - key: 'whatsapp', - width: 800, - height: 800, - alt: () => `npm package ${packageName.value} download chart and stats`, - }, - ], + { alt: () => `npm package ${packageName.value} download chart and stats` }, ) if (import.meta.server) {