Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c0a23b4
now able to detect whether gh releases are biing used by a package.
WilcoSp Feb 4, 2026
4873b1c
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 4, 2026
92193b5
moved has changelog to it's own api endpoint
WilcoSp Feb 7, 2026
2a397bf
added detection of a changelog file on the root of the github repo
WilcoSp Feb 7, 2026
4cfbbfe
Merge branch main into feat/changelog-1
WilcoSp Feb 7, 2026
42802dc
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 7, 2026
9c130bd
github releases are now being show at /package-changes (rendering of …
WilcoSp Feb 8, 2026
a424459
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into f…
WilcoSp Feb 8, 2026
e487292
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 8, 2026
6c7d817
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 11, 2026
ba81215
fixed issue where markdown from github releases could be null
WilcoSp Feb 11, 2026
8d22cf2
some refactoring
WilcoSp Feb 11, 2026
f8b32d4
adding a11y test for changelog card.
WilcoSp Feb 11, 2026
b345df1
markdown is now being rendered (settings need to be added still)
WilcoSp Feb 12, 2026
f4c84ec
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 12, 2026
bc0fa2f
added headings handling and toc (only scroll behavior needs to be cha…
WilcoSp Feb 12, 2026
5e32d04
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into f…
WilcoSp Feb 12, 2026
faf6b3b
fallback to tag for title if name is null
WilcoSp Feb 13, 2026
14c50da
removing   from toc slug and text
WilcoSp Feb 13, 2026
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
25 changes: 25 additions & 0 deletions app/components/Changelog/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<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">
<div class="flex justify-between">
<h2 class="text-1xl sm:text-2xl font-medium min-w-0 break-words py-2">
{{ release.title }}
</h2>
<ReadmeTocDropdown
v-if="release?.toc && release.toc.length > 1"
:toc="release.toc"
class="justify-self-end"
/>
<!-- :active-id="activeTocId" -->
</div>
<Readme v-if="release.html" :html="release.html"></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" -->
12 changes: 12 additions & 0 deletions app/components/Changelog/Releases.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<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" />
</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">
<h1
class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate"
>
<NuxtLink v-if="packageName" :to="packageRoute(packageName, version)">
{{ packageName }}
</NuxtLink>
</h1>

<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 w-full" 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 @@ -195,6 +195,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 @@ -882,6 +883,14 @@ const showSkeleton = shallowRef(false)
{{ $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 @@ -207,7 +207,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 @@ -206,7 +206,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 @@ -206,7 +206,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()}`
// },
// },
)
61 changes: 61 additions & 0 deletions server/api/changelog/releases/[provider]/[owner]/[repo].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ProviderId } from '~~/shared/utils/git-providers'
import type { ReleaseData } from '~~/shared/types/changelog'
import { ERROR_CHANGELOG_RELEASES_FAILED, THROW_INCOMPLETE_PARAM } from '~~/shared/utils/constants'
import { GithubReleaseCollectionSchama } from '~~/shared/schemas/changelog/release'
import { parse } from 'valibot'
import { changelogRenderer } from '~~/server/utils/changelog/markdown'

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

if (!repo || !provider || !owner) {
throw createError({
status: 404,
statusMessage: THROW_INCOMPLETE_PARAM,
})
}

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

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

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

const { releases } = parse(GithubReleaseCollectionSchama, data)

const render = await changelogRenderer()

return releases.map(r => {
const { html, toc } = render(r.markdown, r.id)
return {
id: r.id,
html,
title: r.name ?? r.tag,
draft: r.draft,
prerelease: r.prerelease,
toc,
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