Skip to content

Commit 9c130bd

Browse files
committed
github releases are now being show at /package-changes (rendering of md -> html does need to change)
1 parent 4cfbbfe commit 9c130bd

File tree

11 files changed

+272
-31
lines changed

11 files changed

+272
-31
lines changed

app/components/Changelog/Card.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import type { ReleaseData } from '~~/shared/types/changelog'
3+
4+
const { release } = defineProps<{
5+
release: ReleaseData
6+
}>()
7+
</script>
8+
<template>
9+
<section class="border border-border rounded-lg p-4 sm:p-6">
10+
<h1 class="text-1xl sm:text-2xl font-medium min-w-0 break-words py-2">
11+
{{ release.title }}
12+
</h1>
13+
<Readme :html="release.html.trim()" class="whitespace-pre-line"></Readme>
14+
</section>
15+
</template>
16+
17+
<!-- 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" -->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
const { info } = defineProps<{ info: ChangelogReleaseInfo }>()
3+
4+
const { data: releases } = useFetch<ReleaseData[]>(
5+
() => `/api/changelog/releases/${info.provider}/${info.repo}`,
6+
)
7+
</script>
8+
<template>
9+
<div class="flex flex-col gap-2 py-3" v-if="releases">
10+
<ChangelogCard v-for="release of releases" :release :key="release.id" />
11+
12+
<!-- <ChangelogCard />
13+
<ChangelogCard /> -->
14+
</div>
15+
</template>
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
export function usePackageHasChangelog(
1+
import type { ChangelogInfo } from '~~/shared/types/changelog'
2+
3+
export function usePackageChangelog(
24
packageName: MaybeRefOrGetter<string>,
35
version?: MaybeRefOrGetter<string | null | undefined>,
46
) {
5-
return useLazyFetch<boolean>(() => {
7+
return useLazyFetch<ChangelogInfo | false>(() => {
68
const name = toValue(packageName)
79
const ver = toValue(version)
8-
const base = `/api/changelog/has/${name}`
10+
const base = `/api/changelog/info/${name}`
911
return ver ? `${base}/v/${ver}` : base
1012
})
1113
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script setup lang="ts">
2+
definePageMeta({
3+
name: 'changes',
4+
path: '/package-changes/:path+',
5+
alias: ['/package/changes/:path+', '/changes/:path+'],
6+
})
7+
8+
/// routing
9+
10+
const route = useRoute('changes')
11+
const router = useRouter()
12+
// Parse package name, version, and file path from URL
13+
// Patterns:
14+
// /code/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0", filePath: null (show tree)
15+
// /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
16+
// /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
17+
const parsedRoute = computed(() => {
18+
const segments = route.params.path
19+
20+
// Find the /v/ separator for version
21+
const vIndex = segments.indexOf('v')
22+
if (vIndex === -1 || vIndex >= segments.length - 1) {
23+
// No version specified - redirect or error
24+
return {
25+
packageName: segments.join('/'),
26+
version: null as string | null,
27+
filePath: null as string | null,
28+
}
29+
}
30+
31+
const packageName = segments.slice(0, vIndex).join('/')
32+
const afterVersion = segments.slice(vIndex + 1)
33+
const version = afterVersion[0] ?? null
34+
const filePath = afterVersion.length > 1 ? afterVersion.slice(1).join('/') : null
35+
36+
return { packageName, version, filePath }
37+
})
38+
39+
const packageName = computed(() => parsedRoute.value.packageName)
40+
const version = computed(() => parsedRoute.value.version)
41+
// const filePathOrig = computed(() => parsedRoute.value.filePath)
42+
const filePath = computed(() => parsedRoute.value.filePath?.replace(/\/$/, ''))
43+
44+
const { data: pkg } = usePackage(packageName)
45+
46+
const versionUrlPattern = computed(() => {
47+
const base = `/package-changes/${packageName.value}/v/{version}`
48+
return filePath.value ? `${base}/${filePath.value}` : base
49+
})
50+
51+
const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)
52+
53+
watch(
54+
[version, latestVersion, packageName],
55+
([version, latest, name]) => {
56+
if (!version && latest && name) {
57+
const pathSegments = [...name.split('/'), 'v', latest]
58+
router.replace({ name: 'changes', params: { path: pathSegments as [string, ...string[]] } })
59+
}
60+
},
61+
{ immediate: true },
62+
)
63+
64+
// getting info
65+
66+
const { data: changelog } = usePackageChangelog(packageName, version)
67+
</script>
68+
<template>
69+
<main class="flex-1 flex flex-col">
70+
<header class="border-b border-border bg-bg sticky top-14 z-20">
71+
<div class="container pt-4 pb-3">
72+
<div class="flex items-center gap-2 mb-3 flex-wrap min-w-0">
73+
<NuxtLink
74+
v-if="packageName"
75+
:to="packageRoute(packageName, version)"
76+
class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate"
77+
>
78+
{{ packageName }}
79+
</NuxtLink>
80+
81+
<VersionSelector
82+
v-if="version && pkg?.versions && pkg?.['dist-tags']"
83+
:package-name="packageName"
84+
:current-version="version"
85+
:versions="pkg.versions"
86+
:dist-tags="pkg['dist-tags']"
87+
:url-pattern="versionUrlPattern"
88+
/>
89+
</div>
90+
</div>
91+
</header>
92+
93+
<section class="container" v-if="changelog">
94+
<LazyChangelogReleases v-if="changelog.type == 'release'" :info="changelog" />
95+
<p v-else>changelog.md support is comming or the package doesn't have changelogs</p>
96+
</section>
97+
</main>
98+
</template>

app/pages/package/[[org]]/[name].vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const { data: skillsData } = useLazyFetch<SkillsListResponse>(
118118
119119
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
120120
const { data: moduleReplacement } = useModuleReplacement(packageName)
121-
const { data: hasChangelog } = usePackageHasChangelog(packageName, requestedVersion)
121+
const { data: hasChangelog } = usePackageChangelog(packageName, requestedVersion)
122122
123123
const {
124124
data: resolvedVersion,
@@ -745,8 +745,11 @@ onKeyStroke(
745745
{{ $t('package.links.issues') }}
746746
</LinkBase>
747747
</li>
748-
<li v-if="hasChangelog">
749-
<LinkBase classicon="i-carbon:warning">
748+
<li v-if="!!hasChangelog && resolvedVersion">
749+
<LinkBase
750+
classicon="i-carbon:warning"
751+
:to="{ name: 'changes', params: { path: [pkg.name, 'v', resolvedVersion] } }"
752+
>
750753
{{ $t('package.links.changelog') }}
751754
</LinkBase>
752755
</li>
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import type { ExtendedPackageJson } from '#shared/utils/package-analysis'
22
import { PackageRouteParamsSchema } from '#shared/schemas/package'
3-
import {
4-
ERROR_PACKAGE_HAS_CHANGELOG,
5-
NPM_REGISTRY,
6-
CACHE_MAX_AGE_ONE_DAY,
7-
} from '#shared/utils/constants'
3+
import { ERROR_PACKAGE_DETECT_CHANGELOG, NPM_REGISTRY } from '#shared/utils/constants'
84
import * as v from 'valibot'
9-
import { detectHasChangelog } from '~~/server/utils/has-changelog'
5+
import { detectChangelog } from '~~/server/utils/changelog/detectChangelog'
6+
// CACHE_MAX_AGE_ONE_DAY,
107

118
export default defineCachedEventHandler(
129
async event => {
@@ -26,20 +23,20 @@ export default defineCachedEventHandler(
2623
`${NPM_REGISTRY}/${encodedName}${versionSuffix}`,
2724
)
2825

29-
return !!(await detectHasChangelog(pkg))
26+
return await detectChangelog(pkg)
3027
} catch (error) {
3128
handleApiError(error, {
3229
statusCode: 502,
33-
message: ERROR_PACKAGE_HAS_CHANGELOG,
30+
message: ERROR_PACKAGE_DETECT_CHANGELOG,
3431
})
3532
}
3633
},
37-
{
38-
maxAge: CACHE_MAX_AGE_ONE_DAY, // 24 hours - analysis rarely changes
39-
swr: true,
40-
getKey: event => {
41-
const pkg = getRouterParam(event, 'pkg') ?? ''
42-
return `changelog:v1:${pkg.replace(/\/+$/, '').trim()}`
43-
},
44-
},
34+
// {
35+
// maxAge: CACHE_MAX_AGE_ONE_DAY, // 24 hours - analysis rarely changes
36+
// swr: true,
37+
// getKey: event => {
38+
// const pkg = getRouterParam(event, 'pkg') ?? ''
39+
// return `changelog:v1:${pkg.replace(/\/+$/, '').trim()}`
40+
// },
41+
// },
4542
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { ProviderId } from '~~/shared/utils/git-providers'
2+
import type { ReleaseData } from '~~/shared/types/changelog'
3+
import { GithubReleaseCollectionSchama } from '~~/shared/schemas/changelog/release'
4+
import { ERROR_CHANGELOG_RELEASES_FAILED, THROW_INCOMPLETE_PARAM } from '~~/shared/utils/constants'
5+
import { parse } from 'valibot'
6+
7+
export default defineCachedEventHandler(async event => {
8+
const provider = getRouterParam(event, 'provider')
9+
const repo = getRouterParam(event, 'repo')
10+
11+
if (!repo || !provider || !/^[\w-]+\/[\w-]+$/.test(repo)) {
12+
throw createError({
13+
status: 404,
14+
statusMessage: THROW_INCOMPLETE_PARAM,
15+
})
16+
}
17+
18+
try {
19+
switch (provider as ProviderId) {
20+
case 'github':
21+
return getReleasesFromGithub(repo)
22+
23+
default:
24+
return false
25+
}
26+
} catch (error) {
27+
handleApiError(error, {
28+
statusCode: 502,
29+
// message: 'temp',
30+
message: ERROR_CHANGELOG_RELEASES_FAILED,
31+
})
32+
}
33+
})
34+
35+
async function getReleasesFromGithub(repo: string) {
36+
const data = await $fetch(`https://ungh.cc/repos/${repo}/releases`, {
37+
headers: {
38+
'Accept': '*/*',
39+
'User-Agent': 'npmx.dev',
40+
},
41+
})
42+
43+
const { releases } = parse(GithubReleaseCollectionSchama, data)
44+
45+
return releases.map(
46+
r =>
47+
({
48+
id: r.id,
49+
html: r.html,
50+
title: r.name,
51+
draft: r.draft,
52+
prerelease: r.prerelease,
53+
publishedAt: r.publishedAt,
54+
}) satisfies ReleaseData,
55+
)
56+
}
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type { ChangelogReleaseInfo } from '~~/shared/types/changelog'
2+
import { type RepoRef, parseRepoUrl } from '~~/shared/utils/git-providers'
13
import type { ExtendedPackageJson } from '~~/shared/utils/package-analysis'
2-
import { parseRepoUrl, type RepoRef } from '~~/shared/utils/git-providers'
4+
// ChangelogInfo
35

46
/**
57
* Detect whether changelogs/releases are available for this package
68
*
79
* first checks if releases are available and then changelog.md
810
*/
9-
export async function detectHasChangelog(
11+
export async function detectChangelog(
1012
pkg: ExtendedPackageJson,
1113
// packageName: string,
1214
// version: string,
@@ -20,18 +22,16 @@ export async function detectHasChangelog(
2022
return false
2123
}
2224

23-
if (await checkReleases(repoRef)) {
24-
return true
25-
}
25+
const releaseInfo = await checkReleases(repoRef)
2626

27-
return checkChangelogFile(repoRef)
27+
return releaseInfo || checkChangelogFile(repoRef)
2828
}
2929

3030
/**
3131
* check whether releases are being used with this repo
3232
* @returns true if in use
3333
*/
34-
async function checkReleases(ref: RepoRef): Promise<boolean> {
34+
async function checkReleases(ref: RepoRef): Promise<ChangelogReleaseInfo | false> {
3535
const checkUrls = getLatestReleaseUrl(ref)
3636

3737
for (const checkUrl of checkUrls ?? []) {
@@ -45,7 +45,11 @@ async function checkReleases(ref: RepoRef): Promise<boolean> {
4545
.then(r => r.ok)
4646
.catch(() => false)
4747
if (exists) {
48-
return true
48+
return {
49+
provider: ref.provider,
50+
type: 'release',
51+
repo: `${ref.owner}/${ref.repo}`,
52+
}
4953
}
5054
}
5155
return false
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as v from 'valibot'
2+
3+
export const GithubReleaseSchama = v.object({
4+
id: v.pipe(v.number(), v.integer()),
5+
name: v.string(),
6+
draft: v.boolean(),
7+
prerelease: v.boolean(),
8+
// publishedAt: v.pipe(v.string(), v.isoDateTime()),
9+
html: v.string(),
10+
markdown: v.string(),
11+
publishedAt: v.pipe(v.string(), v.isoTimestamp()),
12+
})
13+
14+
export const GithubReleaseCollectionSchama = v.object({
15+
releases: v.array(GithubReleaseSchama),
16+
})
17+
18+
export type GithubRelease = v.InferOutput<typeof GithubReleaseSchama>
19+
export type GithubReleaseCollection = v.InferOutput<typeof GithubReleaseCollectionSchama>

shared/types/changelog.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ProviderId } from '../utils/git-providers'
2+
3+
export interface ChangelogReleaseInfo {
4+
type: 'release'
5+
provider: ProviderId
6+
repo: `${string}/${string}`
7+
}
8+
9+
export interface ChangelogMarkdownInfo {
10+
type: 'md'
11+
provider: ProviderId
12+
/**
13+
* location within the repository
14+
*/
15+
location: string
16+
}
17+
18+
export type ChangelogInfo = ChangelogReleaseInfo | ChangelogMarkdownInfo
19+
20+
export interface ReleaseData {
21+
title: string // example "v1.x.x",
22+
html: string
23+
prerelease?: boolean
24+
draft?: boolean
25+
id: string | number
26+
publishedAt?: string
27+
}

0 commit comments

Comments
 (0)