Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
5cd51af
adding id for navigation for when it gets implemented
WilcoSp Feb 16, 2026
53cd46f
code blocks, blockquote & links are now handled by the markdown renderer
WilcoSp Feb 16, 2026
30d2663
new lines rendering is now closing to how it's on github. at least fo…
WilcoSp Feb 16, 2026
8d5ee71
enableling gfm for changelogs
WilcoSp Feb 20, 2026
34faad0
the changelog info api endpoint will now give the info when a changel…
WilcoSp Feb 20, 2026
55d45f3
now also able to render changelog markdown files
WilcoSp Feb 20, 2026
9c36c60
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 20, 2026
95e3699
added heading links for changelog markdown
WilcoSp Feb 21, 2026
9f9195d
release card header links can now also be clicked to navigate to
WilcoSp Feb 21, 2026
f2d5f2b
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 22, 2026
cab3cc6
when the latest gh release contains a link to a changelog.md the chan…
WilcoSp Feb 22, 2026
342adca
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 22, 2026
1a8c342
re exporting slugify
WilcoSp Feb 22, 2026
fbbff33
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 23, 2026
08717bc
adding back removing non bracking spaces, :emoji: support now also ad…
WilcoSp Feb 23, 2026
e07af6c
added indication that release is pre-release/draft
WilcoSp Feb 23, 2026
8225654
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 24, 2026
013654c
preventing package@version from being turned into mailto
WilcoSp Feb 24, 2026
6cbe6e8
changed how content-none is applied for changelog.md
WilcoSp Feb 24, 2026
1d93405
for changelog.md now resolving url
WilcoSp Feb 24, 2026
f6ec86e
relative/absolute resolved urls will stay within the base url
WilcoSp Feb 24, 2026
6198a71
id with release id will now be applied to all elements
WilcoSp Feb 24, 2026
2c057b9
Added button to view changelog/releases on the provider website.
WilcoSp Feb 25, 2026
ce34993
added a div which reservers the space for toc
WilcoSp Feb 25, 2026
fd15403
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 25, 2026
04b2a99
adding back .tolowercase in slugify to have test pass again
WilcoSp Feb 25, 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
59 changes: 59 additions & 0 deletions app/components/Changelog/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import type { ReleaseData } from '~~/shared/types/changelog'
import { useDateFormat } from '#imports'

const { release } = defineProps<{
release: ReleaseData
}>()
const formattedDate = useDateFormat(() => release.publishedAt, 'YYYY-MM-DD', {})

const cardId = computed(() => (release.publishedAt ? `date-${formattedDate.value}` : undefined))

const navId = computed(() => `releaae-${encodeURIComponent(release.title)}`)

function navigateToTitle() {
navigateTo(`#${navId.value}`)
}
</script>
<template>
<section class="border border-border rounded-lg p-4 sm:p-6">
<div class="flex gap-2 items-center" :id="cardId">
<h2 class="text-1xl sm:text-2xl font-medium min-w-0 break-words py-2" :id="navId">
<a
class="hover:decoration-accent hover:text-accent focus-visible:decoration-accent focus-visible:text-accent transition-colors duration-200"
:class="$style.linkTitle"
:href="`#${navId}`"
@click.prevent="navigateToTitle()"
>
{{ release.title }}
</a>
</h2>
<TagStatic v-if="release.prerelease" variant="default" :tabindex="0" class="h-unset">
{{ $t('changelog.pre_release') }}
</TagStatic>
<TagStatic v-if="release.draft" variant="default" :tabindex="0" class="h-unset">
{{ $t('changelog.draft') }}
</TagStatic>
<div class="flex-1" aria-hidden="true"></div>
<ReadmeTocDropdown
v-if="release?.toc && release.toc.length > 1"
:toc="release.toc"
class="ms-auto"
/>
<!-- :active-id="activeTocId" -->
</div>
<DateTime v-if="release.publishedAt" :datetime="release.publishedAt" date-style="medium" />
<Readme v-if="release.html" :html="release.html"></Readme>
</section>
</template>

<style module>
.linkTitle::after {
content: '__';
@apply inline i-lucide:link rtl-flip ms-1 opacity-0;
}

.linkTitle:hover:after {
@apply opacity-100;
}
</style>
18 changes: 18 additions & 0 deletions app/components/Changelog/Markdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
const { info, tpTarget } = defineProps<{
info: ChangelogMarkdownInfo
tpTarget?: HTMLElement | null
}>()

const { data } = useLazyFetch(() => `/api/changelog/md/${info.provider}/${info.repo}/${info.path}`)
</script>
<template>
<Teleport
v-if="data?.toc && data.toc.length > 1 && !!tpTarget"
:to="tpTarget"
class="flex justify-end mt-3"
>
<ReadmeTocDropdown :toc="data.toc" class="justify-self-end" />
</Teleport>
<Readme v-if="data?.html" :html="data.html"></Readme>
</template>
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>
2 changes: 1 addition & 1 deletion app/components/Readme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function handleClick(event: MouseEvent) {
@apply inline i-lucide:external-link rtl-flip ms-1 opacity-50;
}

.readme :deep(a[href^='#']::after) {
.readme :deep(a[href^='#']:not([content-none])::after) {
/* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */
content: '__';
@apply inline i-lucide:link rtl-flip ms-1 opacity-0;
Expand Down
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>(() => {
const name = toValue(packageName)
const ver = toValue(version)
const base = `/api/changelog/info/${name}`
return ver ? `${base}/v/${ver}` : base
})
}
22 changes: 22 additions & 0 deletions app/composables/useProviderIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { IconClass } from '~/types/icon'

const PROVIDER_ICONS: Record<string, IconClass> = {
github: 'i-simple-icons:github',
gitlab: 'i-simple-icons:gitlab',
bitbucket: 'i-simple-icons:bitbucket',
codeberg: 'i-simple-icons:codeberg',
gitea: 'i-simple-icons:gitea',
forgejo: 'i-simple-icons:forgejo',
gitee: 'i-simple-icons:gitee',
sourcehut: 'i-simple-icons:sourcehut',
tangled: 'i-custom:tangled',
radicle: 'i-lucide:network', // Radicle is a P2P network, using network icon
}

export function useProviderIcon(provider: MaybeRefOrGetter<ProviderId | null | undefined>) {
return computed((): IconClass => {
const uProvider = toValue(provider)
if (!uProvider) return 'i-simple-icons:github'
return PROVIDER_ICONS[uProvider] ?? 'i-lucide:code'
})
}
2 changes: 1 addition & 1 deletion app/pages/org/[org].vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ defineOgImageComponent('Default', {
target="_blank"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
:title="$t('common.view_on_npm')"
:title="$t('common.view_on', { site: 'npm' })"
>
<span class="i-simple-icons:npm w-4 h-4" aria-hidden="true" />
npm
Expand Down
121 changes: 121 additions & 0 deletions app/pages/package-changes/[...path].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script setup lang="ts">
import { useProviderIcon } from '~/composables/useProviderIcon'

definePageMeta({
name: 'changes',
path: '/package-changes/:path+',
alias: ['/package/changes/:path+', '/changes/:path+'],
scrollMargin: 140,
})

/// 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],
([v, latest, name]) => {
if (!v && 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, pending } = usePackageChangelog(packageName, version)

const repoProviderIcon = useProviderIcon(() => changelog.value?.provider)
const tptoc = useTemplateRef('tptoc')
</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-3 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 class="flex-1"></div>
<LinkBase
v-if="changelog?.link"
:to="changelog?.link"
:classicon="repoProviderIcon"
:title="$t('common.view_on', { site: changelog.provider })"
>
{{ changelog.provider }}
</LinkBase>

<div v-if="changelog?.type == 'md'" ref="tptoc" class="w-14 h-8">
<!-- prevents layout shift while loading -->
</div>
</div>
</div>
</header>

<section class="container w-full" v-if="changelog">
<LazyChangelogReleases v-if="changelog.type == 'release'" :info="changelog" />
<LazyChangelogMarkdown
v-else-if="changelog.type == 'md'"
:info="changelog"
:tpTarget="tptoc"
/>
<p v-else-if="!pending" class="mt-5">{{ $t('changelog.no_logs') }}</p>
</section>
</main>
</template>
32 changes: 12 additions & 20 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
SkillsListResponse,
} from '#shared/types'
import type { JsrPackageInfo } from '#shared/types/jsr'
import type { IconClass } from '~/types'
import { assertValidPackageName } from '#shared/utils/npm'
import { joinURL } from 'ufo'
import { areUrlsEquivalent } from '#shared/utils/url'
Expand All @@ -18,6 +17,7 @@ import { getDependencyCount } from '~/utils/npm/dependency-count'
import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-security'
import { useModal } from '~/composables/useModal'
import { useAtproto } from '~/composables/atproto/useAtproto'
import { useProviderIcon } from '~/composables/useProviderIcon'
import { togglePackageLike } from '~/utils/atproto/likes'
import type { RouteLocationRaw } from 'vue-router'

Expand Down Expand Up @@ -209,6 +209,7 @@ const { data: skillsData } = useLazyFetch<SkillsListResponse>(

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

const { data: resolvedVersion } = await useResolvedVersion(packageName, requestedVersion)

Expand Down Expand Up @@ -412,24 +413,7 @@ const repositoryUrl = computed(() => {

const { meta: repoMeta, repoRef, stars, starsLink, forks, forksLink } = useRepoMeta(repositoryUrl)

const PROVIDER_ICONS: Record<string, IconClass> = {
github: 'i-simple-icons:github',
gitlab: 'i-simple-icons:gitlab',
bitbucket: 'i-simple-icons:bitbucket',
codeberg: 'i-simple-icons:codeberg',
gitea: 'i-simple-icons:gitea',
forgejo: 'i-simple-icons:forgejo',
gitee: 'i-simple-icons:gitee',
sourcehut: 'i-simple-icons:sourcehut',
tangled: 'i-custom:tangled',
radicle: 'i-lucide:network', // Radicle is a P2P network, using network icon
}

const repoProviderIcon = computed((): IconClass => {
const provider = repoRef.value?.provider
if (!provider) return 'i-simple-icons:github'
return PROVIDER_ICONS[provider] ?? 'i-lucide:code'
})
const repoProviderIcon = useProviderIcon(() => repoRef.value?.provider)

const homepageUrl = computed(() => {
const homepage = displayVersion.value?.homepage
Expand Down Expand Up @@ -901,10 +885,18 @@ const showSkeleton = shallowRef(false)
{{ $t('package.links.issues') }}
</LinkBase>
</li>
<li v-if="!!changelog && resolvedVersion">
<LinkBase
classicon="i-lucide:notebook-text"
: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}`"
:title="$t('common.view_on_npm')"
:title="$t('common.view_on', { site: 'npm' })"
classicon="i-simple-icons:npm"
>
npm
Expand Down
2 changes: 1 addition & 1 deletion app/pages/~[username]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ defineOgImageComponent('Default', {
target="_blank"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
:title="$t('common.view_on_npm')"
:title="$t('common.view_on', { site: 'npm' })"
>
<span class="i-simple-icons:npm w-4 h-4" aria-hidden="true" />
npm
Expand Down
2 changes: 1 addition & 1 deletion i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"skip_link": "تخطي إلى المحتوى الرئيسي",
"warnings": "تحذيرات:",
"go_back_home": "العودة إلى الصفحة الرئيسية",
"view_on_npm": "عرض على npm",
"view_on": "عرض على {site}",
"per_week": "/ أسبوع",
"vanity_downloads_hint": "رقم زخرفي: لا توجد حزم معروضة | رقم زخرفي: للحزمة المعروضة | رقم زخرفي: للحزمتين المعروضتين | رقم زخرفي: مجموع {count} من الحزم المعروضة | رقم زخرفي: مجموع {count} من الحزم المعروضة | رقم زخرفي: مجموع {count} من الحزم المعروضة",
"sort": {
Expand Down
2 changes: 1 addition & 1 deletion i18n/locales/az-AZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"skip_link": "Əsas məzmuna keç",
"warnings": "Xəbərdarlıqlar:",
"go_back_home": "Ana səhifəyə qayıt",
"view_on_npm": "npm-də bax",
"view_on": "{site}-də bax",
"per_week": "/ həftə",
"sort": {
"name": "ad",
Expand Down
2 changes: 1 addition & 1 deletion i18n/locales/bg-BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"skip_link": "Преминаване към основното съдържание",
"warnings": "Предупреждения:",
"go_back_home": "Назад към начална страница",
"view_on_npm": "преглед в npm",
"view_on": "преглед в {site}",
"per_week": "/ седмица",
"vanity_downloads_hint": "Брой за показ: няма показани пакети | Брой за показ: за показания пакет | Брой за показ: Сума от {count} показани пакета",
"sort": {
Expand Down
2 changes: 1 addition & 1 deletion i18n/locales/bn-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"skip_link": "মুখ্য কন্টেন্টে যান",
"warnings": "সতর্কতা:",
"go_back_home": "হোমে ফিরে যান",
"view_on_npm": "npm এ দেখুন",
"view_on": "{site} এ দেখুন",
"per_week": "/ সপ্তাহ",
"vanity_downloads_hint": "ভ্যানিটি নম্বর: কোন প্যাকেজ প্রদর্শিত হয়নি | ভ্যানিটি নম্বর: প্রদর্শিত প্যাকেজের জন্য | ভ্যানিটি নম্বর: {count} প্রদর্শিত প্যাকেজের মোট",
"sort": {
Expand Down
2 changes: 1 addition & 1 deletion i18n/locales/cs-CZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"skip_link": "Přejít na hlavní obsah",
"warnings": "Varování:",
"go_back_home": "Zpět na začátek",
"view_on_npm": "Zobrazit na npm",
"view_on": "Zobrazit na {site}",
"per_week": "/ týden",
"vanity_downloads_hint": "Pro zobrazený balíček | Součet pro {count} zobrazené balíčky | Součet pro {count} zobrazených balíčků",
"sort": {
Expand Down
Loading
Loading