Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 =
| (Omit<SlimPackument['requestedVersion'], 'repository'> & { repository?: string | Repository })
| null
Comment on lines +4 to +6

@coderabbitai coderabbitai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

tsc_bin="${TSC_BIN:-tsc}"
command -v "$tsc_bin" >/dev/null

probe="$(mktemp --suffix=.ts)"
trap 'rm -f "$probe"' EXIT

cat >"$probe" <<'EOF'
type Version = { version: string; repository?: { url?: string } } | null
type Broken = Omit<Version, 'repository'>
type Fixed = Omit<Exclude<Version, null>, 'repository'>

declare const broken: Broken
// `@ts-expect-error`: the nullable Omit form must not retain `version`.
const brokenVersion: string = broken.version

declare const fixed: Fixed
const fixedVersion: string = fixed.version

void brokenVersion
void fixedVersion
EOF

"$tsc_bin" --strict --noEmit "$probe"

rg -n 'Omit<.*SlimPackument.*requestedVersion|Omit<Exclude<.*SlimPackument.*requestedVersion' \
  app/composables/useRepositoryUrl.ts \
  test/nuxt/composables/use-repository-url.spec.ts

Repository: npmx-dev/npmx.dev

Length of output: 297


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

tsc_bin="${TSC_BIN:-tsc}"
command -v "$tsc_bin" >/dev/null

probe="$(mktemp --suffix=.ts)"
trap 'rm -f "$probe"' EXIT

cat >"$probe" <<'EOF'
type Version = { version: string; repository?: { url?: string } } | null
type Broken = Omit<Version, 'repository'>
type Fixed = Omit<Exclude<Version, null>, 'repository'>

declare const broken: Broken
// `@ts-expect-error`: the nullable Omit form must not retain `version`.
const brokenVersion: string = broken.version

declare const fixed: Fixed
const fixedVersion: string = fixed.version

void brokenVersion
void fixedVersion
EOF

"$tsc_bin" --strict --noEmit --ignoreConfig "$probe"

printf '%s\n' '--- relevant declarations and uses ---'
rg -n -C 4 \
  'RequestedVersion|requestedVersion|type SlimPackument|interface SlimPackument|type SlimPackumentVersion|interface SlimPackumentVersion' \
  app/composables/useRepositoryUrl.ts \
  shared/types \
  test/nuxt/composables/use-repository-url.spec.ts

Repository: npmx-dev/npmx.dev

Length of output: 4554


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- RequestedVersion references ---'
rg -n -C 3 '\bRequestedVersion\b' app test shared

tsc_bin="${TSC_BIN:-tsc}"
command -v "$tsc_bin" >/dev/null

probe="$(mktemp --suffix=.ts)"
trap 'rm -f "$probe"' EXIT

cat >"$probe" <<'EOF'
type Version = { version: string; name: string; repository?: { url?: string } } | null
type Broken = Omit<Version, 'repository'> & { repository?: string }
type Fixed = Omit<Exclude<Version, null>, 'repository'> & { repository?: string }

const emptyBroken: Broken = {}
// `@ts-expect-error`: the broken type must not expose version.
const brokenVersion: string = ({} as Broken).version

// `@ts-expect-error`: the fixed type requires the version fields.
const emptyFixed: Fixed = {}

const fixed: Fixed = { version: '1.0.0', name: 'pkg' }
const fixedVersion: string = fixed.version

void emptyBroken
void brokenVersion
void emptyFixed
void fixedVersion
EOF

"$tsc_bin" --strict --noEmit --ignoreConfig "$probe"

Repository: npmx-dev/npmx.dev

Length of output: 2112


Exclude null before applying Omit.

SlimPackument['requestedVersion'] is nullable. The current type removes its version fields and accepts {}. Use Omit<Exclude<SlimPackument['requestedVersion'], null>, 'repository'>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/composables/useRepositoryUrl.ts` around lines 4 - 6, Update the
RequestedVersion type alias to exclude null from
SlimPackument['requestedVersion'] before applying Omit, using the existing
repository override and null union so the resulting type does not permit an
empty object from omitting fields on null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm already doing that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilcoSp, the outer | null keeps the nullable result, but it does not exclude null before Omit.

The required change is:

export type RequestedVersion =
  | (Omit<Exclude<SlimPackument['requestedVersion'], null>, 'repository'> & {
      repository?: string | Repository
    })
  | null

This preserves the requested-version fields and still permits null.

You are interacting with an AI system.


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
7 changes: 5 additions & 2 deletions test/nuxt/composables/use-repository-url.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Repository } from '@npm/types'
import { describe, expect, it } from 'vitest'

type RequestedVersion = Exclude<SlimPackument['requestedVersion'], null>
type RequestedVersion = Omit<Exclude<SlimPackument['requestedVersion'], null>, 'repository'> & {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not import the type from app/composables/useRepositoryUrl.ts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to figure things out as it still seems to be that Omit still drops everything in a type instead of only "repository"

but when things with omit are solved, yes it would be possible

repository?: string | Repository
}

function mockPackage(repository: RequestedVersion['repository']): RequestedVersion {
function mockPackage(repository: RequestedVersion['repository'] | string): RequestedVersion {
return {
_id: 'foo',
name: 'foo',
Expand Down
Loading