Skip to content

Commit 8a42360

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/versions-diff-view
# Conflicts: # test/nuxt/a11y.spec.ts
2 parents d1ec938 + a38a2f7 commit 8a42360

File tree

19 files changed

+2317
-70
lines changed

19 files changed

+2317
-70
lines changed

app/assets/main.css

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,17 @@
2424
--border-hover: oklch(0.371 0 0);
2525

2626
/* accent color, set by user from settings */
27-
--accent: var(--accent-color, oklch(1 0 0));
27+
--accent: var(--accent-color, oklch(0.787 0.128 230.318));
2828
--accent-muted: var(--accent-color, oklch(0.922 0 0));
2929

3030
/* accent colors */
31+
--swatch-sky: oklch(0.787 0.128 230.318);
3132
--swatch-coral: oklch(0.704 0.177 14.75);
3233
--swatch-amber: oklch(0.828 0.165 84.429);
3334
--swatch-emerald: oklch(0.792 0.153 166.95);
34-
--swatch-sky: oklch(0.787 0.128 230.318);
3535
--swatch-violet: oklch(0.78 0.148 286.067);
3636
--swatch-magenta: oklch(0.78 0.15 330);
37+
--swatch-neutral: oklch(1 0 0);
3738

3839
/* syntax highlighting colors */
3940
--syntax-fn: oklch(0.727 0.137 299.149);
@@ -94,16 +95,17 @@
9495
--border-subtle: oklch(0.922 0 0);
9596
--border-hover: oklch(0.715 0 0);
9697

97-
--accent: var(--accent-color, oklch(0.145 0 0));
98+
--accent: var(--accent-color, oklch(0.53 0.16 247.27));
9899
--accent-muted: var(--accent-color, oklch(0.205 0 0));
99100

100101
/* accent colors */
101-
--swatch-coral: oklch(0.7 0.19 14.75);
102-
--swatch-amber: oklch(0.8 0.25 84.429);
103-
--swatch-emerald: oklch(0.7 0.17 166.95);
104-
--swatch-sky: oklch(0.7 0.15 230.318);
105-
--swatch-violet: oklch(0.7 0.17 286.067);
106-
--swatch-magenta: oklch(0.75 0.18 330);
102+
--swatch-sky: oklch(0.53 0.16 247.27);
103+
--swatch-coral: oklch(0.56 0.17 10.75);
104+
--swatch-amber: oklch(0.58 0.18 46.34);
105+
--swatch-emerald: oklch(0.51 0.13 162.4);
106+
--swatch-violet: oklch(0.56 0.13 282.067);
107+
--swatch-magenta: oklch(0.56 0.14 325);
108+
--swatch-neutral: oklch(0.145 0 0);
107109

108110
--syntax-fn: oklch(0.502 0.188 294.988);
109111
--syntax-str: oklch(0.425 0.152 252);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import type { InstallSizeDiff } from '~/composables/useInstallSizeDiff'
3+
4+
const props = defineProps<{
5+
diff: InstallSizeDiff
6+
}>()
7+
8+
const bytesFormatter = useBytesFormatter()
9+
const numberFormatter = useNumberFormatter()
10+
11+
const sizePercent = computed(() => Math.round(props.diff.sizeRatio * 100))
12+
</script>
13+
14+
<template>
15+
<div
16+
class="border border-amber-600/40 bg-amber-500/10 rounded-lg px-3 py-2 text-base text-amber-800 dark:text-amber-400"
17+
>
18+
<h2 class="font-medium mb-1 flex items-center gap-2">
19+
<span class="i-lucide:trending-up w-4 h-4" aria-hidden="true" />
20+
{{
21+
diff.sizeThresholdExceeded && diff.depThresholdExceeded
22+
? $t('package.size_increase.title_both', { version: diff.comparisonVersion })
23+
: diff.sizeThresholdExceeded
24+
? $t('package.size_increase.title_size', { version: diff.comparisonVersion })
25+
: $t('package.size_increase.title_deps', { version: diff.comparisonVersion })
26+
}}
27+
</h2>
28+
<p class="text-sm m-0 mt-1">
29+
<i18n-t v-if="diff.sizeThresholdExceeded" keypath="package.size_increase.size" scope="global">
30+
<template #percent
31+
><strong>{{ sizePercent }}%</strong></template
32+
>
33+
<template #size
34+
><strong>{{ bytesFormatter.format(diff.sizeIncrease) }}</strong></template
35+
>
36+
</i18n-t>
37+
<template v-if="diff.sizeThresholdExceeded && diff.depThresholdExceeded"> · </template>
38+
<i18n-t v-if="diff.depThresholdExceeded" keypath="package.size_increase.deps" scope="global">
39+
<template #count
40+
><strong>+{{ numberFormatter.format(diff.depDiff) }}</strong></template
41+
>
42+
</i18n-t>
43+
</p>
44+
</div>
45+
</template>

app/components/Settings/AccentColorPicker.vue

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
55
66
onPrehydrate(el => {
77
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
8+
const defaultId = 'sky'
89
const id = settings.accentColorId
910
if (id) {
1011
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
@@ -13,10 +14,12 @@ onPrehydrate(el => {
1314
input.setAttribute('checked', '')
1415
}
1516
// Remove checked from the server-default (clear button, value="")
16-
const clearInput = el.querySelector<HTMLInputElement>('input[value=""]')
17-
if (clearInput) {
18-
clearInput.checked = false
19-
clearInput.removeAttribute('checked')
17+
if (id !== defaultId) {
18+
const clearInput = el.querySelector<HTMLInputElement>(`input[value="${defaultId}"]`)
19+
if (clearInput) {
20+
clearInput.checked = false
21+
clearInput.removeAttribute('checked')
22+
}
2023
}
2124
}
2225
})
@@ -31,31 +34,19 @@ onPrehydrate(el => {
3134
v-for="color in accentColors"
3235
:key="color.id"
3336
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
37+
:class="color.id === 'neutral' ? 'flex items-center justify-center bg-fg' : ''"
3438
:style="{ backgroundColor: `var(--swatch-${color.id})` }"
3539
>
3640
<input
3741
type="radio"
3842
name="accent-color"
3943
class="sr-only"
4044
:value="color.id"
41-
:checked="selectedAccentColor === color.id"
42-
:aria-label="color.name"
45+
:checked="selectedAccentColor === color.id || (!selectedAccentColor && color.id === 'sky')"
46+
:aria-label="color.id === 'neutral' ? $t('settings.clear_accent') : color.name"
4347
@change="setAccentColor(color.id)"
4448
/>
45-
</label>
46-
<label
47-
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) flex items-center justify-center bg-fg"
48-
>
49-
<input
50-
type="radio"
51-
name="accent-color"
52-
class="sr-only"
53-
value=""
54-
:checked="selectedAccentColor === null"
55-
:aria-label="$t('settings.clear_accent')"
56-
@change="setAccentColor(null)"
57-
/>
58-
<span class="i-lucide:ban size-4 text-bg" aria-hidden="true" />
49+
<span v-if="color.id === 'neutral'" class="i-lucide:ban size-4 text-bg" aria-hidden="true" />
5950
</label>
6051
</fieldset>
6152
</template>

app/components/Settings/BgThemePicker.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@ const { backgroundThemes, selectedBackgroundTheme, setBackgroundTheme } = useBac
33
44
onPrehydrate(el => {
55
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
6+
const defaultId = 'neutral'
67
const id = settings.preferredBackgroundTheme
78
if (id) {
89
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
910
if (input) {
1011
input.checked = true
1112
input.setAttribute('checked', '')
1213
}
14+
// Remove checked from the server-default (clear button, value="")
15+
if (id !== defaultId) {
16+
const clearInput = el.querySelector<HTMLInputElement>(`input[value="${defaultId}"]`)
17+
if (clearInput) {
18+
clearInput.checked = false
19+
clearInput.removeAttribute('checked')
20+
}
21+
}
1322
}
1423
})
1524
</script>
@@ -30,7 +39,10 @@ onPrehydrate(el => {
3039
name="background-theme"
3140
class="sr-only"
3241
:value="theme.id"
33-
:checked="selectedBackgroundTheme === theme.id"
42+
:checked="
43+
selectedBackgroundTheme === theme.id ||
44+
(!selectedBackgroundTheme && theme.id === 'neutral')
45+
"
3446
:aria-label="theme.name"
3547
@change="setBackgroundTheme(theme.id)"
3648
/>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { compare, prerelease, valid } from 'semver'
2+
import type { InstallSizeResult, SlimPackument } from '#shared/types'
3+
4+
export interface InstallSizeDiff {
5+
comparisonVersion: string
6+
sizeRatio: number
7+
sizeIncrease: number
8+
currentSize: number
9+
previousSize: number
10+
depDiff: number
11+
currentDeps: number
12+
previousDeps: number
13+
sizeThresholdExceeded: boolean
14+
depThresholdExceeded: boolean
15+
}
16+
17+
const SIZE_INCREASE_THRESHOLD = 0.25
18+
const DEP_INCREASE_THRESHOLD = 5
19+
20+
function getComparisonVersion(pkg: SlimPackument, resolvedVersion: string): string | null {
21+
const isCurrentPrerelease = prerelease(resolvedVersion) !== null
22+
23+
if (isCurrentPrerelease) {
24+
const latest = pkg['dist-tags']?.latest
25+
if (!latest || latest === resolvedVersion) return null
26+
return latest
27+
}
28+
29+
// Find the previous version in time that was stable
30+
const stableVersions = Object.keys(pkg.time)
31+
.filter(v => v !== 'modified' && v !== 'created' && valid(v) !== null && prerelease(v) === null)
32+
.sort((a, b) => compare(a, b))
33+
34+
const currentIdx = stableVersions.indexOf(resolvedVersion)
35+
if (currentIdx <= 0) return null
36+
37+
return stableVersions[currentIdx - 1]!
38+
}
39+
40+
export function useInstallSizeDiff(
41+
packageName: MaybeRefOrGetter<string>,
42+
resolvedVersion: MaybeRefOrGetter<string | null | undefined>,
43+
pkg: MaybeRefOrGetter<SlimPackument | null | undefined>,
44+
currentInstallSize: MaybeRefOrGetter<InstallSizeResult | null | undefined>,
45+
) {
46+
const comparisonVersion = computed<string | null>(() => {
47+
const pkgVal = toValue(pkg)
48+
const version = toValue(resolvedVersion)
49+
if (!pkgVal || !version) return null
50+
return getComparisonVersion(pkgVal, version)
51+
})
52+
53+
const {
54+
data: comparisonInstallSize,
55+
status: comparisonStatus,
56+
execute: fetchComparisonSize,
57+
} = useLazyFetch<InstallSizeResult | null>(
58+
() => {
59+
const v = comparisonVersion.value
60+
if (!v) return ''
61+
return `/api/registry/install-size/${toValue(packageName)}/v/${v}`
62+
},
63+
{
64+
server: false,
65+
immediate: false,
66+
default: () => null,
67+
},
68+
)
69+
70+
if (import.meta.client) {
71+
watch(
72+
[comparisonVersion, () => toValue(packageName)],
73+
([v]) => {
74+
if (v) fetchComparisonSize()
75+
},
76+
{ immediate: true },
77+
)
78+
}
79+
80+
const diff = computed<InstallSizeDiff | null>(() => {
81+
const current = toValue(currentInstallSize)
82+
const previous = comparisonInstallSize.value
83+
const cv = comparisonVersion.value
84+
85+
if (!current || !previous || !cv) return null
86+
const name = toValue(packageName)
87+
const version = toValue(resolvedVersion)
88+
if (previous.version !== cv || previous.package !== name) return null
89+
if (current.version !== version || current.package !== name) return null
90+
91+
const sizeRatio =
92+
previous.totalSize > 0 ? (current.totalSize - previous.totalSize) / previous.totalSize : 0
93+
const depDiff = current.dependencyCount - previous.dependencyCount
94+
95+
const sizeThresholdExceeded = sizeRatio > SIZE_INCREASE_THRESHOLD
96+
const depThresholdExceeded = depDiff > DEP_INCREASE_THRESHOLD
97+
98+
if (!sizeThresholdExceeded && !depThresholdExceeded) return null
99+
100+
return {
101+
comparisonVersion: cv,
102+
sizeRatio,
103+
sizeIncrease: current.totalSize - previous.totalSize,
104+
currentSize: current.totalSize,
105+
previousSize: previous.totalSize,
106+
depDiff,
107+
currentDeps: current.dependencyCount,
108+
previousDeps: previous.dependencyCount,
109+
sizeThresholdExceeded,
110+
depThresholdExceeded,
111+
}
112+
})
113+
114+
return { diff, comparisonVersion, comparisonStatus }
115+
}

app/pages/package/[[org]]/[name].vue

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type {
3+
InstallSizeResult,
34
NpmVersionDist,
45
PackageVersionInfo,
56
PackumentVersion,
@@ -19,6 +20,7 @@ import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-securi
1920
import { useModal } from '~/composables/useModal'
2021
import { useAtproto } from '~/composables/atproto/useAtproto'
2122
import { togglePackageLike } from '~/utils/atproto/likes'
23+
import { useInstallSizeDiff } from '~/composables/useInstallSizeDiff'
2224
import type { RouteLocationRaw } from 'vue-router'
2325
2426
defineOgImageComponent('Package', {
@@ -182,13 +184,6 @@ const { data: jsrInfo } = useLazyFetch<JsrPackageInfo>(() => `/api/jsr/${package
182184
})
183185
184186
// Fetch total install size (lazy, can be slow for large dependency trees)
185-
interface InstallSizeResult {
186-
package: string
187-
version: string
188-
selfSize: number
189-
totalSize: number
190-
dependencyCount: number
191-
}
192187
const {
193188
data: installSize,
194189
status: installSizeStatus,
@@ -255,6 +250,8 @@ const {
255250
error,
256251
} = usePackage(packageName, () => resolvedVersion.value ?? requestedVersion.value)
257252
253+
const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg, installSize)
254+
258255
// Detect two hydration scenarios where the external _payload.json is missing:
259256
//
260257
// 1. SPA fallback (200.html): No real content was server-rendered.
@@ -1376,6 +1373,8 @@ const showSkeleton = shallowRef(false)
13761373
<div class="space-y-6" :class="$style.areaVulns">
13771374
<!-- Bad package warning -->
13781375
<PackageReplacement v-if="moduleReplacement" :replacement="moduleReplacement" />
1376+
<!-- Size / dependency increase notice -->
1377+
<PackageSizeIncrease v-if="sizeDiff" :diff="sizeDiff" />
13791378
<!-- Vulnerability scan -->
13801379
<ClientOnly>
13811380
<PackageVulnerabilityTree

config/i18n.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ const locales: (LocaleObjectData | (Omit<LocaleObjectData, 'code'> & { code: str
172172
file: 'hi-IN.json',
173173
name: 'हिंदी',
174174
},
175+
{
176+
code: 'kn-IN',
177+
file: 'kn-IN.json',
178+
name: 'ಕನ್ನಡ',
179+
},
175180
{
176181
code: 'te-IN',
177182
file: 'te-IN.json',

i18n/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@
157157
"version": "This version has been deprecated.",
158158
"no_reason": "No reason provided"
159159
},
160+
"size_increase": {
161+
"title_size": "Significant size increase since v{version}",
162+
"title_deps": "Significant dependency count increase since v{version}",
163+
"title_both": "Significant size and dependency increase since v{version}",
164+
"size": "Install size increased by {percent} ({size} larger)",
165+
"deps": "{count} more dependencies"
166+
},
160167
"replacement": {
161168
"title": "You might not need this dependency.",
162169
"native": "This can be replaced with {replacement}, available since Node {nodeVersion}.",

0 commit comments

Comments
 (0)