Skip to content

Commit 42fb7b0

Browse files
aisiklarautofix-ci[bot]ghostdevv43081jgameroman
authored
feat: add a warning when the package license changes (#2188)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Willow (GHOST) <git@willow.sh> Co-authored-by: James Garbutt <43081j@users.noreply.github.com> Co-authored-by: Roman <dev@rman.dev>
1 parent 4269128 commit 42fb7b0

8 files changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script setup lang="ts">
2+
import type { LicenseChangeResponse } from '~/composables/useLicenseChanges'
3+
4+
const props = defineProps<{
5+
change: LicenseChangeResponse['change']
6+
}>()
7+
</script>
8+
9+
<template>
10+
<div
11+
v-if="props.change"
12+
class="border border-amber-600/40 bg-amber-500/10 rounded-lg mt-1 gap-x-1 py-2 px-3"
13+
:aria-label="$t('package.versions.license_change_help')"
14+
>
15+
<p class="text-md text-amber-800 dark:text-amber-400 flex items-center gap-2">
16+
<span
17+
class="i-lucide:alert-triangle w-4 h-4 flex-shrink-0"
18+
role="img"
19+
:aria-label="$t('package.versions.license_change_help')"
20+
/>
21+
{{ $t('package.versions.license_change_warning') }}
22+
</p>
23+
<p class="text-md text-amber-800 dark:text-amber-400 mt-1">
24+
{{
25+
$t('package.versions.license_change_record', {
26+
from: props.change?.from,
27+
to: props.change?.to,
28+
})
29+
}}
30+
</p>
31+
</div>
32+
</template>
33+
34+
<style scoped></style>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { MaybeRefOrGetter } from 'vue'
2+
import { toValue } from 'vue'
3+
4+
export interface LicenseChangeResponse {
5+
change: { from: string; to: string } | null
6+
}
7+
8+
/**
9+
* Composable to detect license changes across all versions of a package
10+
*/
11+
export function useLicenseChanges(
12+
packageName: MaybeRefOrGetter<string | null | undefined>,
13+
resolvedVersion: MaybeRefOrGetter<string | null | undefined> = () => undefined,
14+
) {
15+
const name = computed(() => toValue(packageName))
16+
if (!name) return { data: null } // Don't fetch if no name
17+
18+
const version = computed(() => toValue(resolvedVersion) ?? 'latest')
19+
20+
const url = computed(() => {
21+
return name.value ? `/api/registry/license-change/${encodeURIComponent(name.value)}` : ''
22+
})
23+
24+
const result = useFetch<LicenseChangeResponse>(url, {
25+
query: computed(() => ({ version: version.value })),
26+
key: `license-change:${name.value}:${version.value}`,
27+
watch: [name, version],
28+
})
29+
30+
return result
31+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ const {
204204
error,
205205
} = usePackage(packageName, () => resolvedVersion.value ?? requestedVersion.value)
206206
207+
const { data: licenseChangeData } = useLicenseChanges(packageName, resolvedVersion)
207208
const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg, installSize)
208209
const { versions: commandPaletteVersions, ensureLoaded: ensureCommandPaletteVersionsLoaded } =
209210
useCommandPalettePackageVersions(packageName)
@@ -909,6 +910,8 @@ const showSkeleton = shallowRef(false)
909910
</section>
910911

911912
<div class="space-y-6" :class="$style.areaVulns">
913+
<!-- license change warning -->
914+
<LicenseChangeWarning :change="licenseChangeData?.change ?? null" />
912915
<!-- Bad package warning -->
913916
<PackageReplacement
914917
v-if="moduleReplacement"

i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,9 @@
567567
"filter_help": "Semver range filter help",
568568
"filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.",
569569
"filter_tooltip_link": "semver range",
570+
"license_change_help": "License Change Details",
571+
"license_change_warning": "License changed since previous version.",
572+
"license_change_record": "The license of this package changed from \"{from}\" to \"{to}\".",
570573
"no_matches": "No versions match this range",
571574
"copy_alt": {
572575
"per_version_analysis": "{version} version was downloaded {downloads} times",

i18n/locales/tr-TR.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,9 @@
387387
"filter_help": "Semver aralığı filtresi yardımı",
388388
"filter_tooltip": "Sürümleri {link} kullanarak filtreleyin. Örneğin, ^3.0.0 tüm 3.x sürümlerini gösterir.",
389389
"filter_tooltip_link": "semver aralığı",
390+
"license_change_help": "Lisans Değişikliği Detayı",
391+
"license_change_warning": "Önceki versiyondan sonra lisans değişikliği gerçekleşti.",
392+
"license_change_record": "Lisans \"{from}\"dan \"{to}\"a değişti.",
390393
"no_matches": "Bu aralığa uygun sürüm yok",
391394
"copy_alt": {
392395
"per_version_analysis": "{version} sürümü {downloads} kez indirildi",

i18n/schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,6 +1705,15 @@
17051705
"filter_tooltip_link": {
17061706
"type": "string"
17071707
},
1708+
"license_change_help": {
1709+
"type": "string"
1710+
},
1711+
"license_change_warning": {
1712+
"type": "string"
1713+
},
1714+
"license_change_record": {
1715+
"type": "string"
1716+
},
17081717
"no_matches": {
17091718
"type": "string"
17101719
},
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
interface LicenseChangeRecord {
2+
from: string
3+
to: string
4+
}
5+
6+
export default defineCachedEventHandler(
7+
async event => {
8+
// 1. Extract the package name from the catch-all parameter
9+
const packageName = getRouterParam(event, 'pkg')
10+
if (!packageName) {
11+
throw createError({
12+
statusCode: 400,
13+
statusMessage: 'Package name is required',
14+
})
15+
}
16+
const query = getQuery(event)
17+
const version = query.version || 'latest'
18+
19+
try {
20+
// 2. Fetch the "Packument" on the server
21+
// This stays on the server, so the client never downloads this massive JSON
22+
const data = await fetchNpmPackage(packageName)
23+
24+
if (!data.versions || !data.time) {
25+
throw createError({
26+
statusCode: 404,
27+
statusMessage: 'Package metadata not found',
28+
})
29+
}
30+
// 3. Process the logic
31+
const versions = Object.values(data.versions)
32+
33+
// Sort versions chronologically using the 'time' object
34+
versions.sort((a, b) => {
35+
const timeA = new Date(data.time[a.version] as string).getTime()
36+
const timeB = new Date(data.time[b.version] as string).getTime()
37+
return timeA - timeB
38+
})
39+
let change: LicenseChangeRecord | null = null
40+
41+
const currentVersionIndex =
42+
version === 'latest' ? versions.length - 1 : versions.findIndex(v => v.version === version)
43+
44+
const previousVersionIndex = currentVersionIndex - 1
45+
const currentLicense = String(versions[currentVersionIndex]?.license || 'UNKNOWN')
46+
const previousLicense = String(versions[previousVersionIndex]?.license || 'UNKNOWN')
47+
48+
if (currentLicense !== previousLicense) {
49+
change = {
50+
from: previousLicense,
51+
to: currentLicense,
52+
}
53+
}
54+
return { change }
55+
} catch (error: any) {
56+
throw createError({
57+
statusCode: error.statusCode || 500,
58+
statusMessage: `Failed to fetch license data: ${error.message}`,
59+
})
60+
}
61+
},
62+
{
63+
// 5. Cache Configuration
64+
maxAge: 60 * 60, // time in seconds
65+
swr: true,
66+
getKey: event => {
67+
const pkg = getRouterParam(event, 'pkg') ?? ''
68+
const query = getQuery(event)
69+
70+
// 1. remove the /'s from the package name
71+
const cleanPkg = pkg.replace(/\/+$/, '').trim()
72+
73+
// 2. Get the version (default to 'latest' if not provided)
74+
const version = query.version || 'latest'
75+
76+
// 3. Create a unique string such that it takes into account the pckage name and version
77+
// sample result: "license-change:v1:faker:2.1.15"
78+
return `license-change:v2:${cleanPkg}:${version}`
79+
},
80+
},
81+
)

test/nuxt/a11y.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ import {
252252
PackageSelectionView,
253253
PackageSelectionCheckbox,
254254
PackageExternalLinks,
255+
LicenseChangeWarning,
255256
ChartSplitSparkline,
256257
TabRoot,
257258
TabList,
@@ -385,6 +386,21 @@ describe('component accessibility audits', () => {
385386
})
386387
})
387388

389+
describe('LicenseChangeWarning', () => {
390+
it('should have no accessibility violations', async () => {
391+
const component = await mountSuspended(LicenseChangeWarning, {
392+
props: {
393+
change: { from: 'MIT', to: 'GPL-3.0' },
394+
},
395+
global: {
396+
mocks: { $t: (key: string) => key },
397+
stubs: { 'i18n-t': { template: '<span><slot name="license_change" /></span>' } },
398+
},
399+
})
400+
const results = await runAxe(component)
401+
expect(results.violations).toEqual([])
402+
})
403+
})
388404
describe('AppLogo', () => {
389405
it('should have no accessibility violations', async () => {
390406
const component = await mountSuspended(AppLogo)

0 commit comments

Comments
 (0)