Skip to content
Open
7 changes: 4 additions & 3 deletions app/components/OgImage/Package.takumi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions app/composables/useRepositoryUrl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Repository } from '@npm/types'
import { joinURL } from 'ufo'

type RequestedVersion = SlimPackument['requestedVersion'] | null
export type RequestedVersion =
| SlimPackument['requestedVersion']
| { repository?: string | Repository }

type UseRepositoryUrlReturn = {
repositoryUrl: ComputedRef<string | null>
Expand All @@ -12,10 +15,14 @@ export function useRepositoryUrl(
const repositoryUrl = computed<string | null>(() => {
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
Expand Down
23 changes: 10 additions & 13 deletions server/api/registry/analysis/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!baseRef || !createRef) return false
if (baseRef.provider !== createRef.provider) return false
Expand Down
14 changes: 6 additions & 8 deletions server/utils/changelog/detectChangelog.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -20,21 +20,19 @@ type SafeResult<R, E = Error> = [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 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
}
Expand Down
2 changes: 1 addition & 1 deletion shared/types/npm-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'bugs'?: { url?: string; email?: string }
'storybook'?: { url: string }
/** current version */
Expand Down
2 changes: 1 addition & 1 deletion shared/utils/package-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
15 changes: 14 additions & 1 deletion test/nuxt/composables/use-repository-url.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RequestedVersion as RequestedVersionNullable } from '~/composables/useRepositoryUrl'
import { describe, expect, it } from 'vitest'

type RequestedVersion = Exclude<SlimPackument['requestedVersion'], null>
type RequestedVersion = NonNullable<RequestedVersionNullable>

function mockPackage(repository: RequestedVersion['repository']): RequestedVersion {
return {
Expand Down Expand Up @@ -66,4 +67,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')
})
})
Loading