From e3b7ed859b7319b94514bff55a3b752e35c2a8d4 Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 16:16:35 +0200 Subject: [PATCH 1/8] fixed getting repo info for changelog --- server/utils/changelog/detectChangelog.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/server/utils/changelog/detectChangelog.ts b/server/utils/changelog/detectChangelog.ts index 926b71e143..3448dc60b1 100644 --- a/server/utils/changelog/detectChangelog.ts +++ b/server/utils/changelog/detectChangelog.ts @@ -1,6 +1,6 @@ import type { ChangelogMarkdownInfo, ChangelogInfo } from '~~/shared/types/changelog' import type { ExtendedPackageJson } from '~~/shared/utils/package-analysis' -import { type RepoRef, parseRepoUrl } from '~~/shared/utils/git-providers' +import { type RepoRef, parseRepositoryInfo } from '~~/shared/utils/git-providers' import { type RepoFileUrl, getBaseFileUrl } from './baseFileUrl' import { FetchError } from 'ofetch' import { ERROR_CHANGELOG_NOT_FOUND, ERROR_UNGH_API_KEY_EXHAUSTED } from '~~/shared/utils/constants' @@ -20,21 +20,17 @@ type SafeResult = [R, null] | [null, E] * first checks if releases are available and then changelog.md */ export async function detectChangelog(pkg: ExtendedPackageJson) { - if (!pkg.repository?.url) { - return false - } - - const repoRef = parseRepoUrl(pkg.repository.url) + const repoRef = parseRepositoryInfo(pkg.repository) if (!repoRef) { return false } - const [releases, releasesError] = await checkReleases(repoRef, pkg.repository.directory) + const [releases, releasesError] = await checkReleases(repoRef, pkg.repository?.directory) if (releases) { return releases } - const changelog = await checkChangelogFile(repoRef, pkg.repository.directory) + const changelog = await checkChangelogFile(repoRef, pkg.repository?.directory) if (changelog) { return changelog } From 168f2a00dcc3b99cd040705da09fa3f9e9532ed9 Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 17:11:21 +0200 Subject: [PATCH 2/8] fixed getting repo url at package page when repo is the url instead of an object --- app/composables/useRepositoryUrl.ts | 6 +++++- test/nuxt/composables/use-repository-url.spec.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/composables/useRepositoryUrl.ts b/app/composables/useRepositoryUrl.ts index 0e65aa0ee6..b0d652a1ce 100644 --- a/app/composables/useRepositoryUrl.ts +++ b/app/composables/useRepositoryUrl.ts @@ -12,10 +12,14 @@ export function useRepositoryUrl( const repositoryUrl = computed(() => { const repo = toValue(requestedVersion)?.repository + if (typeof repo === 'string') { + // sometimes repo can be a string due to not being normalized during publishing + return normalizeGitUrl(repo) + } + if (!repo?.url) { return null } - let url = normalizeGitUrl(repo.url) if (!url) { return null diff --git a/test/nuxt/composables/use-repository-url.spec.ts b/test/nuxt/composables/use-repository-url.spec.ts index b66ff46ef6..d03d3f9a2e 100644 --- a/test/nuxt/composables/use-repository-url.spec.ts +++ b/test/nuxt/composables/use-repository-url.spec.ts @@ -66,4 +66,16 @@ describe('useRepositoryUrl', () => { expect(repositoryUrl.value).toBe('https://github.com/org/repo/tree/HEAD/packages/core/') }) + + it('should handle shorthand url', () => { + const { repositoryUrl } = useRepositoryUrl(mockPackage('https://github.com/nuxt/ui')) + expect(repositoryUrl.value).toBe('https://github.com/nuxt/ui') + }) + + it('should strip .git from shorthand repo url', () => { + const { repositoryUrl } = useRepositoryUrl( + mockPackage('git+https://github.com/agentmarkup/agentmarkup.git'), + ) + expect(repositoryUrl.value).toBe('https://github.com/agentmarkup/agentmarkup') + }) }) From e12058d5407d0da051f3c5f23ee39218113ee5d7 Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 19:14:01 +0200 Subject: [PATCH 3/8] package analysis now also allows string & object --- server/api/registry/analysis/[...pkg].get.ts | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/server/api/registry/analysis/[...pkg].get.ts b/server/api/registry/analysis/[...pkg].get.ts index 3c23102dfd..73cc42e512 100644 --- a/server/api/registry/analysis/[...pkg].get.ts +++ b/server/api/registry/analysis/[...pkg].get.ts @@ -15,7 +15,7 @@ import { CACHE_MAX_AGE_ONE_DAY, ERROR_PACKAGE_ANALYSIS_FAILED, } from '#shared/utils/constants' -import { parseRepoUrl } from '#shared/utils/git-providers' +import { parseRepositoryInfo } from '#shared/utils/git-providers' import { encodePackageName } from '#shared/utils/npm' import { fetchPackageWithTypesAndFiles } from '#server/utils/file-tree' import { getLatestVersionBatch } from 'fast-npm-meta' @@ -147,30 +147,27 @@ async function fetchCreatePackageForValidation( * Check if two packages are associated (share maintainers or same repo owner). */ function isAssociatedPackage( - basePkg: { maintainers?: Array<{ name: string }>; repository?: { url?: string } }, - createPkg: { maintainers?: Array<{ name: string }>; repository?: { url?: string } }, + basePkg: { maintainers?: Array<{ name: string }>; repository?: { url?: string } | string }, + createPkg: { maintainers?: Array<{ name: string }>; repository?: { url?: string } | string }, ): boolean { const baseMaintainers = new Set(basePkg.maintainers?.map(m => m.name.toLowerCase()) ?? []) const createMaintainers = createPkg.maintainers?.map(m => m.name.toLowerCase()) ?? [] const hasSharedMaintainer = createMaintainers.some(name => baseMaintainers.has(name)) - return ( - hasSharedMaintainer || - hasSameRepositoryOwner(basePkg.repository?.url, createPkg.repository?.url) - ) + return hasSharedMaintainer || hasSameRepositoryOwner(basePkg.repository, createPkg.repository) } /** * Check if two repository URLs have the same owner (works with any git provider). */ function hasSameRepositoryOwner( - baseRepoUrl: string | undefined, - createRepoUrl: string | undefined, + baseRepo: string | { url?: string } | undefined, + createRepo: string | { url?: string } | undefined, ): boolean { - if (!baseRepoUrl || !createRepoUrl) return false + if (!baseRepo || !createRepo) return false - const baseRef = parseRepoUrl(baseRepoUrl) - const createRef = parseRepoUrl(createRepoUrl) + const baseRef = parseRepositoryInfo(baseRepo) + const createRef = parseRepositoryInfo(createRepo) if (!baseRef || !createRef) return false if (baseRef.provider !== createRef.provider) return false From 6911a5107dd31e07f63021d2854a9a1166e84c8f Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 21:13:59 +0200 Subject: [PATCH 4/8] added more support for repository to be a string --- app/components/OgImage/Package.takumi.vue | 7 ++++--- server/utils/changelog/detectChangelog.ts | 6 ++++-- shared/types/npm-registry.ts | 2 +- shared/utils/package-analysis.ts | 2 +- test/nuxt/composables/use-repository-url.spec.ts | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/OgImage/Package.takumi.vue b/app/components/OgImage/Package.takumi.vue index 8597c7bca3..adf9bb11a7 100644 --- a/app/components/OgImage/Package.takumi.vue +++ b/app/components/OgImage/Package.takumi.vue @@ -54,10 +54,11 @@ const versionLabel = computed(() => (version ? `v${version}` : '')) const repositoryUrl = computed(() => { const repo = displayVersion.value?.repository - if (!repo?.url) return null - let url = normalizeGitUrl(repo.url) + const repoUrl = typeof repo === 'object' ? repo.url : repo + if (!repoUrl) return null + let url = normalizeGitUrl(repoUrl) // append `repository.directory` for monorepo packages - if (repo.directory) { + if (typeof repo === 'object' && repo.directory) { url = joinURL(`${url}/tree/HEAD`, repo.directory) } return url diff --git a/server/utils/changelog/detectChangelog.ts b/server/utils/changelog/detectChangelog.ts index 3448dc60b1..6289a7aebd 100644 --- a/server/utils/changelog/detectChangelog.ts +++ b/server/utils/changelog/detectChangelog.ts @@ -25,12 +25,14 @@ export async function detectChangelog(pkg: ExtendedPackageJson) { return false } - const [releases, releasesError] = await checkReleases(repoRef, pkg.repository?.directory) + const directory = typeof pkg.repository === 'object' ? pkg.repository.directory : undefined + + const [releases, releasesError] = await checkReleases(repoRef, directory) if (releases) { return releases } - const changelog = await checkChangelogFile(repoRef, pkg.repository?.directory) + const changelog = await checkChangelogFile(repoRef, directory) if (changelog) { return changelog } diff --git a/shared/types/npm-registry.ts b/shared/types/npm-registry.ts index 97c4d99903..46ec7caee1 100644 --- a/shared/types/npm-registry.ts +++ b/shared/types/npm-registry.ts @@ -84,7 +84,7 @@ export interface SlimPackument { 'license'?: string 'homepage'?: string 'keywords'?: string[] - 'repository'?: { type?: string; url?: string; directory?: string } + 'repository'?: { type?: string; url?: string; directory?: string } | string 'bugs'?: { url?: string; email?: string } 'storybook'?: { url: string } /** current version */ diff --git a/shared/utils/package-analysis.ts b/shared/utils/package-analysis.ts index d9c0a39f15..6807a02499 100644 --- a/shared/utils/package-analysis.ts +++ b/shared/utils/package-analysis.ts @@ -37,7 +37,7 @@ export interface ExtendedPackageJson { /** npm maintainers (returned by registry API) */ maintainers?: Array<{ name: string; email?: string }> /** Repository info (returned by registry API) */ - repository?: { url?: string; type?: string; directory?: string } + repository?: { url?: string; type?: string; directory?: string } | string } export type PackageExports = string | null | { [key: string]: PackageExports } | PackageExports[] diff --git a/test/nuxt/composables/use-repository-url.spec.ts b/test/nuxt/composables/use-repository-url.spec.ts index d03d3f9a2e..bf537fb968 100644 --- a/test/nuxt/composables/use-repository-url.spec.ts +++ b/test/nuxt/composables/use-repository-url.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' type RequestedVersion = Exclude -function mockPackage(repository: RequestedVersion['repository']): RequestedVersion { +function mockPackage(repository: RequestedVersion['repository'] | string): RequestedVersion { return { _id: 'foo', name: 'foo', From 3b1643c2b19b226d2cfa9023dc5915a88e646563 Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 22:23:56 +0200 Subject: [PATCH 5/8] .. --- server/api/registry/analysis/[...pkg].get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/registry/analysis/[...pkg].get.ts b/server/api/registry/analysis/[...pkg].get.ts index 73cc42e512..0d44ded81a 100644 --- a/server/api/registry/analysis/[...pkg].get.ts +++ b/server/api/registry/analysis/[...pkg].get.ts @@ -68,7 +68,7 @@ export default defineCachedEventHandler( /** Package metadata needed for association validation */ interface PackageWithMeta { maintainers?: Array<{ name: string }> - repository?: { url?: string } + repository?: { url?: string } | string deprecated?: string } From 643428470263bfc1394236207dc39add56464e93 Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 23:06:25 +0200 Subject: [PATCH 6/8] revert --- app/components/OgImage/Package.takumi.vue | 7 ++++--- server/api/registry/analysis/[...pkg].get.ts | 2 +- server/utils/changelog/detectChangelog.ts | 6 ++++-- shared/types/npm-registry.ts | 2 +- shared/utils/package-analysis.ts | 2 +- test/nuxt/composables/use-repository-url.spec.ts | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/components/OgImage/Package.takumi.vue b/app/components/OgImage/Package.takumi.vue index 8597c7bca3..adf9bb11a7 100644 --- a/app/components/OgImage/Package.takumi.vue +++ b/app/components/OgImage/Package.takumi.vue @@ -54,10 +54,11 @@ const versionLabel = computed(() => (version ? `v${version}` : '')) const repositoryUrl = computed(() => { const repo = displayVersion.value?.repository - if (!repo?.url) return null - let url = normalizeGitUrl(repo.url) + const repoUrl = typeof repo === 'object' ? repo.url : repo + if (!repoUrl) return null + let url = normalizeGitUrl(repoUrl) // append `repository.directory` for monorepo packages - if (repo.directory) { + if (typeof repo === 'object' && repo.directory) { url = joinURL(`${url}/tree/HEAD`, repo.directory) } return url diff --git a/server/api/registry/analysis/[...pkg].get.ts b/server/api/registry/analysis/[...pkg].get.ts index 73cc42e512..0d44ded81a 100644 --- a/server/api/registry/analysis/[...pkg].get.ts +++ b/server/api/registry/analysis/[...pkg].get.ts @@ -68,7 +68,7 @@ export default defineCachedEventHandler( /** Package metadata needed for association validation */ interface PackageWithMeta { maintainers?: Array<{ name: string }> - repository?: { url?: string } + repository?: { url?: string } | string deprecated?: string } diff --git a/server/utils/changelog/detectChangelog.ts b/server/utils/changelog/detectChangelog.ts index 3448dc60b1..6289a7aebd 100644 --- a/server/utils/changelog/detectChangelog.ts +++ b/server/utils/changelog/detectChangelog.ts @@ -25,12 +25,14 @@ export async function detectChangelog(pkg: ExtendedPackageJson) { return false } - const [releases, releasesError] = await checkReleases(repoRef, pkg.repository?.directory) + const directory = typeof pkg.repository === 'object' ? pkg.repository.directory : undefined + + const [releases, releasesError] = await checkReleases(repoRef, directory) if (releases) { return releases } - const changelog = await checkChangelogFile(repoRef, pkg.repository?.directory) + const changelog = await checkChangelogFile(repoRef, directory) if (changelog) { return changelog } diff --git a/shared/types/npm-registry.ts b/shared/types/npm-registry.ts index 97c4d99903..46ec7caee1 100644 --- a/shared/types/npm-registry.ts +++ b/shared/types/npm-registry.ts @@ -84,7 +84,7 @@ export interface SlimPackument { 'license'?: string 'homepage'?: string 'keywords'?: string[] - 'repository'?: { type?: string; url?: string; directory?: string } + 'repository'?: { type?: string; url?: string; directory?: string } | string 'bugs'?: { url?: string; email?: string } 'storybook'?: { url: string } /** current version */ diff --git a/shared/utils/package-analysis.ts b/shared/utils/package-analysis.ts index d9c0a39f15..6807a02499 100644 --- a/shared/utils/package-analysis.ts +++ b/shared/utils/package-analysis.ts @@ -37,7 +37,7 @@ export interface ExtendedPackageJson { /** npm maintainers (returned by registry API) */ maintainers?: Array<{ name: string; email?: string }> /** Repository info (returned by registry API) */ - repository?: { url?: string; type?: string; directory?: string } + repository?: { url?: string; type?: string; directory?: string } | string } export type PackageExports = string | null | { [key: string]: PackageExports } | PackageExports[] diff --git a/test/nuxt/composables/use-repository-url.spec.ts b/test/nuxt/composables/use-repository-url.spec.ts index d03d3f9a2e..bf537fb968 100644 --- a/test/nuxt/composables/use-repository-url.spec.ts +++ b/test/nuxt/composables/use-repository-url.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' type RequestedVersion = Exclude -function mockPackage(repository: RequestedVersion['repository']): RequestedVersion { +function mockPackage(repository: RequestedVersion['repository'] | string): RequestedVersion { return { _id: 'foo', name: 'foo', From 55d8e8f53ebc054984adeab647f25390db28176c Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Wed, 12 Aug 2026 23:13:00 +0200 Subject: [PATCH 7/8] .. --- test/nuxt/composables/use-repository-url.spec.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/test/nuxt/composables/use-repository-url.spec.ts b/test/nuxt/composables/use-repository-url.spec.ts index bf537fb968..b66ff46ef6 100644 --- a/test/nuxt/composables/use-repository-url.spec.ts +++ b/test/nuxt/composables/use-repository-url.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' type RequestedVersion = Exclude -function mockPackage(repository: RequestedVersion['repository'] | string): RequestedVersion { +function mockPackage(repository: RequestedVersion['repository']): RequestedVersion { return { _id: 'foo', name: 'foo', @@ -66,16 +66,4 @@ describe('useRepositoryUrl', () => { expect(repositoryUrl.value).toBe('https://github.com/org/repo/tree/HEAD/packages/core/') }) - - it('should handle shorthand url', () => { - const { repositoryUrl } = useRepositoryUrl(mockPackage('https://github.com/nuxt/ui')) - expect(repositoryUrl.value).toBe('https://github.com/nuxt/ui') - }) - - it('should strip .git from shorthand repo url', () => { - const { repositoryUrl } = useRepositoryUrl( - mockPackage('git+https://github.com/agentmarkup/agentmarkup.git'), - ) - expect(repositoryUrl.value).toBe('https://github.com/agentmarkup/agentmarkup') - }) }) From dbd8252d2d5a7fcfd46c7c2deb07bd4d60cc1b4b Mon Sep 17 00:00:00 2001 From: WilcoSp Date: Thu, 13 Aug 2026 00:03:30 +0200 Subject: [PATCH 8/8] added type override closer to useRepositoryUrl --- app/composables/useRepositoryUrl.ts | 5 ++++- test/nuxt/composables/use-repository-url.spec.ts | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/composables/useRepositoryUrl.ts b/app/composables/useRepositoryUrl.ts index b0d652a1ce..83ee640556 100644 --- a/app/composables/useRepositoryUrl.ts +++ b/app/composables/useRepositoryUrl.ts @@ -1,6 +1,9 @@ +import type { Repository } from '@npm/types' import { joinURL } from 'ufo' -type RequestedVersion = SlimPackument['requestedVersion'] | null +export type RequestedVersion = + | (Omit & { repository?: string | Repository }) + | null type UseRepositoryUrlReturn = { repositoryUrl: ComputedRef diff --git a/test/nuxt/composables/use-repository-url.spec.ts b/test/nuxt/composables/use-repository-url.spec.ts index b66ff46ef6..260c6ab00e 100644 --- a/test/nuxt/composables/use-repository-url.spec.ts +++ b/test/nuxt/composables/use-repository-url.spec.ts @@ -1,8 +1,11 @@ +import type { Repository } from '@npm/types' import { describe, expect, it } from 'vitest' -type RequestedVersion = Exclude +type RequestedVersion = Omit, 'repository'> & { + repository?: string | Repository +} -function mockPackage(repository: RequestedVersion['repository']): RequestedVersion { +function mockPackage(repository: RequestedVersion['repository'] | string): RequestedVersion { return { _id: 'foo', name: 'foo',