Skip to content
17 changes: 17 additions & 0 deletions app/components/Changelog/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { ReleaseData } from '~~/shared/types/changelog'

const { release } = defineProps<{
release: ReleaseData
}>()
</script>
<template>
<section class="border border-border rounded-lg p-4 sm:p-6">
<h1 class="text-1xl sm:text-2xl font-medium min-w-0 break-words py-2">
{{ release.title }}
</h1>
<Readme :html="release.html.trim()" class="whitespace-pre-line"></Readme>
</section>
</template>

<!-- class="group bg-bg-subtle border border-border rounded-lg p-4 sm:p-6 transition-[border-color,background-color] duration-200 hover:(border-border-hover bg-bg-muted) cursor-pointer relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 focus-within:bg-bg-muted focus-within:border-border-hover" -->
15 changes: 15 additions & 0 deletions app/components/Changelog/Releases.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
const { info } = defineProps<{ info: ChangelogReleaseInfo }>()

const { data: releases } = useFetch<ReleaseData[]>(
() => `/api/changelog/releases/${info.provider}/${info.repo}`,
)
</script>
<template>
<div class="flex flex-col gap-2 py-3" v-if="releases">
<ChangelogCard v-for="release of releases" :release :key="release.id" />

<!-- <ChangelogCard />
<ChangelogCard /> -->
</div>
</template>
13 changes: 13 additions & 0 deletions app/composables/usePackageChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ChangelogInfo } from '~~/shared/types/changelog'

export function usePackageChangelog(
packageName: MaybeRefOrGetter<string>,
version?: MaybeRefOrGetter<string | null | undefined>,
) {
return useLazyFetch<ChangelogInfo | false>(() => {
const name = toValue(packageName)
const ver = toValue(version)
const base = `/api/changelog/info/${name}`
return ver ? `${base}/v/${ver}` : base
})
}
98 changes: 98 additions & 0 deletions app/pages/package-changes/[...path].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
definePageMeta({
name: 'changes',
path: '/package-changes/:path+',
alias: ['/package/changes/:path+', '/changes/:path+'],
})

/// routing

const route = useRoute('changes')
const router = useRouter()
// Parse package name, version, and file path from URL
// Patterns:
// /code/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0", filePath: null (show tree)
// /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
// /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
const parsedRoute = computed(() => {
const segments = route.params.path

// Find the /v/ separator for version
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
// No version specified - redirect or error
return {
packageName: segments.join('/'),
version: null as string | null,
filePath: null as string | null,
}
}

const packageName = segments.slice(0, vIndex).join('/')
const afterVersion = segments.slice(vIndex + 1)
const version = afterVersion[0] ?? null
const filePath = afterVersion.length > 1 ? afterVersion.slice(1).join('/') : null

return { packageName, version, filePath }
})

const packageName = computed(() => parsedRoute.value.packageName)
const version = computed(() => parsedRoute.value.version)
// const filePathOrig = computed(() => parsedRoute.value.filePath)
const filePath = computed(() => parsedRoute.value.filePath?.replace(/\/$/, ''))

const { data: pkg } = usePackage(packageName)

const versionUrlPattern = computed(() => {
const base = `/package-changes/${packageName.value}/v/{version}`
return filePath.value ? `${base}/${filePath.value}` : base
})

const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)

watch(
[version, latestVersion, packageName],
([version, latest, name]) => {
if (!version && latest && name) {
const pathSegments = [...name.split('/'), 'v', latest]
router.replace({ name: 'changes', params: { path: pathSegments as [string, ...string[]] } })
}
},
{ immediate: true },
)

// getting info

const { data: changelog } = usePackageChangelog(packageName, version)
</script>
<template>
<main class="flex-1 flex flex-col">
<header class="border-b border-border bg-bg sticky top-14 z-20">
<div class="container pt-4 pb-3">
<div class="flex items-center gap-2 mb-3 flex-wrap min-w-0">
<NuxtLink
v-if="packageName"
:to="packageRoute(packageName, version)"
class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate"
>
{{ packageName }}
</NuxtLink>

<VersionSelector
v-if="version && pkg?.versions && pkg?.['dist-tags']"
:package-name="packageName"
:current-version="version"
:versions="pkg.versions"
:dist-tags="pkg['dist-tags']"
:url-pattern="versionUrlPattern"
/>
</div>
</div>
</header>

<section class="container" v-if="changelog">
<LazyChangelogReleases v-if="changelog.type == 'release'" :info="changelog" />
<p v-else>changelog.md support is comming or the package doesn't have changelogs</p>
</section>
</main>
</template>
9 changes: 9 additions & 0 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const { data: skillsData } = useLazyFetch<SkillsListResponse>(

const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
const { data: moduleReplacement } = useModuleReplacement(packageName)
const { data: hasChangelog } = usePackageChangelog(packageName, requestedVersion)

const {
data: resolvedVersion,
Expand Down Expand Up @@ -745,6 +746,14 @@ onKeyStroke(
{{ $t('package.links.issues') }}
</LinkBase>
</li>
<li v-if="!!hasChangelog && resolvedVersion">
<LinkBase
classicon="i-carbon:warning"
:to="{ name: 'changes', params: { path: [pkg.name, 'v', resolvedVersion] } }"
>
{{ $t('package.links.changelog') }}
</LinkBase>
</li>
<li>
<LinkBase
:to="`https://www.npmjs.com/package/${pkg.name}`"
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
"code": "code",
"docs": "docs",
"fund": "fund",
"compare": "compare"
"compare": "compare",
"changelog": "changelog"
},
"likes": {
"like": "Like this package",
Expand Down
3 changes: 2 additions & 1 deletion lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
"code": "code",
"docs": "docs",
"fund": "fund",
"compare": "compare"
"compare": "compare",
"changelog": "changelog"
},
"likes": {
"like": "Like this package",
Expand Down
3 changes: 2 additions & 1 deletion lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
"code": "code",
"docs": "docs",
"fund": "fund",
"compare": "compare"
"compare": "compare",
"changelog": "changelog"
},
"likes": {
"like": "Like this package",
Expand Down
42 changes: 42 additions & 0 deletions server/api/changelog/info/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ExtendedPackageJson } from '#shared/utils/package-analysis'
import { PackageRouteParamsSchema } from '#shared/schemas/package'
import { ERROR_PACKAGE_DETECT_CHANGELOG, NPM_REGISTRY } from '#shared/utils/constants'
import * as v from 'valibot'
import { detectChangelog } from '~~/server/utils/changelog/detectChangelog'
// CACHE_MAX_AGE_ONE_DAY,

export default defineCachedEventHandler(
async event => {
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []

const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments)

try {
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
packageName: rawPackageName,
version: rawVersion,
})

const encodedName = encodePackageName(packageName)
const versionSuffix = version ? `/${version}` : '/latest'
const pkg = await $fetch<ExtendedPackageJson>(
`${NPM_REGISTRY}/${encodedName}${versionSuffix}`,
)

return await detectChangelog(pkg)
} catch (error) {
handleApiError(error, {
statusCode: 502,
message: ERROR_PACKAGE_DETECT_CHANGELOG,
})
}
},
// {
// maxAge: CACHE_MAX_AGE_ONE_DAY, // 24 hours - analysis rarely changes
// swr: true,
// getKey: event => {
// const pkg = getRouterParam(event, 'pkg') ?? ''
// return `changelog:v1:${pkg.replace(/\/+$/, '').trim()}`
// },
// },
)
56 changes: 56 additions & 0 deletions server/api/changelog/releases/[provider]/[...repo].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ProviderId } from '~~/shared/utils/git-providers'
import type { ReleaseData } from '~~/shared/types/changelog'
import { GithubReleaseCollectionSchama } from '~~/shared/schemas/changelog/release'
import { ERROR_CHANGELOG_RELEASES_FAILED, THROW_INCOMPLETE_PARAM } from '~~/shared/utils/constants'
import { parse } from 'valibot'

export default defineCachedEventHandler(async event => {
const provider = getRouterParam(event, 'provider')
const repo = getRouterParam(event, 'repo')

if (!repo || !provider || !/^[\w-]+\/[\w-]+$/.test(repo)) {
throw createError({
status: 404,
statusMessage: THROW_INCOMPLETE_PARAM,
})
}

try {
switch (provider as ProviderId) {
case 'github':
return getReleasesFromGithub(repo)

default:
return false
}
} catch (error) {
handleApiError(error, {
statusCode: 502,
// message: 'temp',
message: ERROR_CHANGELOG_RELEASES_FAILED,
})
}
})

async function getReleasesFromGithub(repo: string) {
const data = await $fetch(`https://ungh.cc/repos/${repo}/releases`, {
headers: {
'Accept': '*/*',
'User-Agent': 'npmx.dev',
},
})

const { releases } = parse(GithubReleaseCollectionSchama, data)

return releases.map(
r =>
({
id: r.id,
html: r.html,
title: r.name,
draft: r.draft,
prerelease: r.prerelease,
publishedAt: r.publishedAt,
}) satisfies ReleaseData,
)
}
1 change: 0 additions & 1 deletion server/api/registry/analysis/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default defineCachedEventHandler(
const createPackage = await findAssociatedCreatePackage(packageName, pkg)

const analysis = analyzePackage(pkg, { typesPackage, createPackage })

return {
package: packageName,
version: pkg.version ?? version ?? 'latest',
Expand Down
Loading
Loading