Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions app/components/LicenseDisplay.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
<script setup lang="ts">
import { parseLicenseExpression } from '#shared/utils/spdx'

import { useLicenseChanges } from '~/composables/useLicenseChanges'

const props = defineProps<{
license: string
packageName?: string
}>()

const tokens = computed(() => parseLicenseExpression(props.license))
const licenseChanges = useLicenseChanges(() => props.packageName)

const changes = computed(() => licenseChanges.data.value?.changes ?? [])

const licenseChangeText = computed(() =>
changes.value
.map(item =>
$t('package.versions.license_change_item', {
from: item.from,
to: item.to,
version: item.version,
}),
)
.join('; '),
)

const hasAnyValidLicense = computed(() => tokens.value.some(t => t.type === 'license' && t.url))
</script>
Expand All @@ -32,4 +50,31 @@ const hasAnyValidLicense = computed(() => tokens.value.some(t => t.type === 'lic
aria-hidden="true"
/>
</span>
<div
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, the warning should be here like the package size increase one

Image

v-if="changes.length > 0"
class="border border-amber-600/40 bg-amber-500/10 rounded-lg inline-flex justify-start items-center mt-1 gap-x-1 py-[2px] px-[3px]"
>
<p class="text-md text-amber-800 dark:text-amber-400">
{{ $t('package.versions.license_change_warning') }}
</p>
<TooltipApp interactive position="top">
<span
tabindex="0"
class="block cursor-help shrink-0 -m-2 p-2 -me-1 focus-visible:outline-2 focus-visible:outline-accent/70 rounded"
>
<span
class="block i-lucide:info w-3.5 h-3.5 text-fg-subtle"
role="img"
:aria-label="$t('package.versions.license_change_help')"
/>
</span>
<template #content>
<p class="text-xs text-fg-muted">
<i18n-t keypath="package.versions.changed_license" tag="span">
<template #license_change>{{ licenseChangeText }}</template>
</i18n-t>
</p>
</template>
</TooltipApp>
</div>
</template>
74 changes: 74 additions & 0 deletions app/composables/useLicenseChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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<string, string>
versions: Record<string, NpmRegistryVersion>
}

/**
* Composable to detect license changes across all versions of a package
*/
export function useLicenseChanges(packageName: MaybeRefOrGetter<string | null | undefined>) {
return useAsyncData<LicenseChangesResult>(
() => `license-changes:${toValue(packageName)}`,
async () => {
const name = toValue(packageName)
if (!name) return { changes: [] }

// Fetch full package metadata from npm registry
const url = `https://registry.npmjs.org/${name}`
const data = await $fetch<NpmRegistryResponse>(url)

const changes: LicenseChange[] = []
let prevLicense: string | undefined = undefined

// `data.versions` is an object with version keys
const versions = Object.values(data.versions) as NpmRegistryVersion[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only compare to the last version, there is some logic here that could be reused (you can move it into its own utility file in shared too

function getComparisonVersion(pkg: SlimPackument, resolvedVersion: string): string | null {
const isCurrentPrerelease = prerelease(resolvedVersion) !== null
if (isCurrentPrerelease) {
const latest = pkg['dist-tags']?.latest
if (!latest || latest === resolvedVersion) return null
return latest
}
// Find the previous version in time that was stable
const stableVersions = Object.keys(pkg.time)
.filter(v => v !== 'modified' && v !== 'created' && valid(v) !== null && prerelease(v) === null)
.sort((a, b) => compare(a, b))
const currentIdx = stableVersions.indexOf(resolvedVersion)
// Don't compare the second version against the first as the first
// has no baseline so a large size difference is expected
if (currentIdx <= 1) return null
return stableVersions[currentIdx - 1]!
}


// 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
})

// Detect license changes
for (const version of versions) {
const license = (version.license as string) ?? 'UNKNOWN'
if (prevLicense && license !== prevLicense) {
changes.push({
from: prevLicense,
to: license,
version: version.version as string,
})
}
prevLicense = license
}
return { changes }
},
{
default: () => ({ changes: [] }),
watch: [() => toValue(packageName)],
},
)
}
6 changes: 5 additions & 1 deletion app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,11 @@ const showSkeleton = shallowRef(false)
{{ $t('package.stats.license') }}
</dt>
<dd class="font-mono text-sm text-fg">
<LicenseDisplay v-if="pkg.license" :license="pkg.license" />
<LicenseDisplay
v-if="pkg.license"
:license="pkg.license"
:package-name="pkg.name"
/>
<span v-else>{{ $t('package.license.none') }}</span>
</dd>
</div>
Expand Down
4 changes: 4 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "change!",
"filter_tooltip_link": "semver range",
"no_matches": "No versions match this range",
"copy_alt": {
Expand Down
4 changes: 4 additions & 0 deletions i18n/locales/tr-TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "değişiklik!",
"filter_tooltip_link": "semver aralığı",
"no_matches": "Bu aralığa uygun sürüm yok",
"copy_alt": {
Expand Down
12 changes: 12 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading