Skip to content
Merged
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
53 changes: 30 additions & 23 deletions app/components/ClaimPackageModal.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import type { CheckNameResult } from '~/utils/package-name'
import { checkPackageName } from '~/utils/package-name'

const props = defineProps<{
Expand All @@ -16,25 +15,34 @@ const {
refreshState,
} = useConnector()

// Fetch name availability when modal opens
const checkResult = shallowRef<CheckNameResult | null>(null)

const isChecking = shallowRef(false)
const isPublishing = shallowRef(false)
const publishError = shallowRef<string | null>(null)
const publishSuccess = shallowRef(false)
const publishError = shallowRef<string | null>(null)

async function checkAvailability() {
isChecking.value = true
publishError.value = null
try {
checkResult.value = await checkPackageName(props.packageName)
} catch (err) {
publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_check')
} finally {
isChecking.value = false
}
}
const {
data: checkResult,
refresh: checkAvailability,
status,
error: checkError,
} = useAsyncData(
(_nuxtApp, { signal }) => {
return checkPackageName(props.packageName, { signal })
},
{ default: () => null, immediate: false },
)

const isChecking = computed(() => {
return status.value === 'pending'
})

const mergedError = computed(() => {
return (
publishError.value ??
(checkError.value instanceof Error
? checkError.value.message
: $t('claim.modal.failed_to_check'))
)
})

const connectorModal = useModal('connector-modal')

Expand Down Expand Up @@ -92,7 +100,6 @@ const dialogRef = ref<HTMLDialogElement>()

function open() {
// Reset state and check availability each time modal is opened
checkResult.value = null
publishError.value = null
publishSuccess.value = false
checkAvailability()
Expand Down Expand Up @@ -287,11 +294,11 @@ const previewPackageJson = computed(() => {

<!-- Error message -->
<div
v-if="publishError"
v-if="mergedError"
role="alert"
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
>
{{ publishError }}
{{ mergedError }}
</div>

<!-- Actions -->
Expand Down Expand Up @@ -369,17 +376,17 @@ const previewPackageJson = computed(() => {
</div>

<!-- Error state -->
<div v-else-if="publishError" class="space-y-4">
<div v-else-if="mergedError" class="space-y-4">
<div
role="alert"
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
>
{{ publishError }}
{{ mergedError }}
</div>
<button
type="button"
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="checkAvailability"
@click="() => checkAvailability()"
>
{{ $t('common.retry') }}
</button>
Expand Down
8 changes: 6 additions & 2 deletions app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ const packumentCache = new Map<string, Promise<Packument | null>>()
* Uses bulk API for unscoped packages, parallel individual requests for scoped.
* Note: npm bulk downloads API does not support scoped packages.
*/
async function fetchBulkDownloads(packageNames: string[]): Promise<Map<string, number>> {
async function fetchBulkDownloads(
packageNames: string[],
options: Parameters<typeof $fetch>[1] = {},
): Promise<Map<string, number>> {
const downloads = new Map<string, number>()
if (packageNames.length === 0) return downloads

Expand All @@ -44,6 +47,7 @@ async function fetchBulkDownloads(packageNames: string[]): Promise<Map<string, n
try {
const response = await $fetch<Record<string, { downloads: number } | null>>(
`${NPM_API}/downloads/point/last-week/${chunk.join(',')}`,
options,
)
for (const [name, data] of Object.entries(response)) {
if (data?.downloads !== undefined) {
Expand Down Expand Up @@ -572,7 +576,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
return results
})(),
// Fetch downloads in bulk
fetchBulkDownloads(packageNames),
fetchBulkDownloads(packageNames, { signal }),
])

// Convert to search results with download data
Expand Down
22 changes: 16 additions & 6 deletions app/utils/package-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ export interface CheckNameResult {

const NPM_REGISTRY = 'https://registry.npmjs.org'

export async function checkPackageExists(name: string): Promise<boolean> {
export async function checkPackageExists(
name: string,
options: Parameters<typeof $fetch>[1] = {},
): Promise<boolean> {
try {
const encodedName = name.startsWith('@')
? `@${encodeURIComponent(name.slice(1))}`
: encodeURIComponent(name)

await $fetch(`${NPM_REGISTRY}/${encodedName}`, {
...options,
method: 'HEAD',
})
return true
Expand All @@ -87,7 +91,10 @@ export async function checkPackageExists(name: string): Promise<boolean> {
}
}

export async function findSimilarPackages(name: string): Promise<SimilarPackage[]> {
export async function findSimilarPackages(
name: string,
options: Parameters<typeof $fetch>[1] = {},
): Promise<SimilarPackage[]> {
const normalized = normalizePackageName(name)
const similar: SimilarPackage[] = []

Expand All @@ -99,7 +106,7 @@ export async function findSimilarPackages(name: string): Promise<SimilarPackage[
description?: string
}
}>
}>(`${NPM_REGISTRY}/-/v1/search?text=${encodeURIComponent(name)}&size=20`)
}>(`${NPM_REGISTRY}/-/v1/search?text=${encodeURIComponent(name)}&size=20`, options)

for (const obj of searchResponse.objects) {
const pkgName = obj.package.name
Expand Down Expand Up @@ -153,7 +160,10 @@ export async function findSimilarPackages(name: string): Promise<SimilarPackage[
}
}

export async function checkPackageName(name: string): Promise<CheckNameResult> {
export async function checkPackageName(
name: string,
options: Parameters<typeof $fetch>[1] = {},
): Promise<CheckNameResult> {
const validation = validatePackageName(name)
const valid = validation.validForNewPackages === true

Expand All @@ -177,8 +187,8 @@ export async function checkPackageName(name: string): Promise<CheckNameResult> {

// Check if package exists and find similar packages in parallel
const [exists, similarPackages] = await Promise.all([
checkPackageExists(name),
findSimilarPackages(name),
checkPackageExists(name, options),
findSimilarPackages(name, options),
])

result.available = !exists
Expand Down
Loading