diff --git a/app/components/LicenseChangeWarning.vue b/app/components/LicenseChangeWarning.vue new file mode 100644 index 0000000000..0cad600d63 --- /dev/null +++ b/app/components/LicenseChangeWarning.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/app/composables/useLicenseChanges.ts b/app/composables/useLicenseChanges.ts new file mode 100644 index 0000000000..24554fffb9 --- /dev/null +++ b/app/composables/useLicenseChanges.ts @@ -0,0 +1,88 @@ +import type { MaybeRefOrGetter } from 'vue' +import { toValue } from 'vue' + +export interface LicenseChange { + from: string + to: string + version: string +} + +export interface LicenseChangesResult { + changes: LicenseChange[] +} + +// Type definitions for npm registry response +interface NpmRegistryVersion { + version: string + license?: string +} + +// for registry responses of $fetch function, the type includes the key versions as well as many others too. +interface NpmRegistryResponse { + time: Record + versions: Record +} + +/** + * Composable to detect license changes across all versions of a package + */ +export function useLicenseChanges( + packageName: MaybeRefOrGetter, + resolvedVersion: MaybeRefOrGetter = () => undefined, +) { + return useAsyncData( + () => { + const name = toValue(packageName) + const version = toValue(resolvedVersion) ?? 'latest' + return `license-changes:${name}:${version}` + }, + async () => { + const name = toValue(packageName) + const resolvedVer = toValue(resolvedVersion) + if (!name) return { changes: [] } + + // Fetch full package metadata from npm registry + const url = `https://registry.npmjs.org/${name}` + const data = await $fetch(url) + + const changes: LicenseChange[] = [] + + // `data.versions` is an object with version keys + const versions = Object.values(data.versions) as NpmRegistryVersion[] + + // Sort versions ascending to compare chronologically + versions.sort((a, b) => { + const dateA = new Date(data.time[a.version] as string).getTime() + const dateB = new Date(data.time[b.version] as string).getTime() + + // Ascending order (oldest to newest) + return dateA - dateB + }) + + // When resolvedVer is not provided, check changes across all versions + const targetVersion = resolvedVer ?? versions[versions.length - 1]?.version + + if (targetVersion) { + const resolvedIndex = versions.findIndex(v => v.version === targetVersion) + + if (resolvedIndex > 0) { + const currentLicense = (versions[resolvedIndex]?.license as string) ?? 'UNKNOWN' + const previousLicense = (versions[resolvedIndex - 1]?.license as string) ?? 'UNKNOWN' + + if (currentLicense !== previousLicense) { + changes.push({ + from: previousLicense, + to: currentLicense, + version: (versions[resolvedIndex]?.version as string) || 'UNKNOWN', + }) + } + } + } + return { changes } + }, + { + default: () => ({ changes: [] }), + watch: [() => toValue(packageName), () => toValue(resolvedVersion)], + }, + ) +} diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 4bc4d85bca..488d0ea5d2 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -883,6 +883,8 @@ const showSkeleton = shallowRef(false)
+ + diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 66f0eb39b5..00f5edd373 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -432,7 +432,11 @@ "filter_placeholder": "Filter by semver (e.g. ^3.0.0)", "filter_invalid": "Invalid semver range", "filter_help": "Semver range filter help", + "license_change_help": "License Change Details", + "license_change_item": "from {from} to {to} at version {version}", "filter_tooltip": "Filter versions using a {link}. For example, ^3.0.0 shows all 3.x versions.", + "changed_license": "The license was changed {license_change}", + "license_change_warning": "License change!", "filter_tooltip_link": "semver range", "no_matches": "No versions match this range", "copy_alt": { diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 8d686950a3..a83ef6f268 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -386,7 +386,11 @@ "filter_placeholder": "Semver ile filtrele (örn. ^3.0.0)", "filter_invalid": "Geçersiz semver aralığı", "filter_help": "Semver aralığı filtresi yardımı", + "license_change_help": "Lisans değişikliği yardımı", + "license_change_item": "{version} sürümünde {from}'den {to}'ya", "filter_tooltip": "Sürümleri {link} kullanarak filtreleyin. Örneğin, ^3.0.0 tüm 3.x sürümlerini gösterir.", + "changed_license": "Lisans değişikliği gerçekleşti: {license_change}", + "license_change_warning": "Lisans değişikliği!", "filter_tooltip_link": "semver aralığı", "no_matches": "Bu aralığa uygun sürüm yok", "copy_alt": { diff --git a/i18n/schema.json b/i18n/schema.json index 35f289f1fb..0398321ab1 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1300,9 +1300,21 @@ "filter_help": { "type": "string" }, + "license_change_help": { + "type": "string" + }, + "license_change_item": { + "type": "string" + }, "filter_tooltip": { "type": "string" }, + "changed_license": { + "type": "string" + }, + "license_change_warning": { + "type": "string" + }, "filter_tooltip_link": { "type": "string" }, diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 46bba7bba1..1c433e0019 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -235,6 +235,7 @@ import { PackageSelectionView, PackageSelectionCheckbox, PackageExternalLinks, + LicenseChangeWarning, ChartSplitSparkline, TabRoot, TabList, @@ -349,6 +350,22 @@ describe('component accessibility audits', () => { }) }) + describe('LicenseChangeWarning', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(LicenseChangeWarning, { + props: { + packageName: 'vue', + resolvedVersion: '3.4.0', + }, + global: { + mocks: { $t: (key: string) => key }, + stubs: { 'i18n-t': { template: '' } }, + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) describe('AppLogo', () => { it('should have no accessibility violations', async () => { const component = await mountSuspended(AppLogo)