Skip to content
Open
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: 6 additions & 1 deletion app/components/Code/Header.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { PackageFileContentResponse } from '#shared/types/npm-registry'
import { getPackageFileViewerUrl } from '#shared/utils/package-files'

interface BreadcrumbItem {
name: string
Expand All @@ -26,6 +27,10 @@ const emit = defineEmits<{

const { toggleCodeContainer } = useCodeContainer()

const rawFileUrl = computed(() =>
props.filePath ? getPackageFileViewerUrl(props.packageName, props.version, props.filePath) : '',
)

const markdownViewModes = [
{
key: 'preview' as const,
Expand Down Expand Up @@ -244,7 +249,7 @@ useEventListener('keydown', (event: KeyboardEvent) => {
<TooltipApp :text="$t('code.open_raw_file')" position="top">
<LinkBase
variant="button-secondary"
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
:to="rawFileUrl"
class="px-3"
:aria-label="$t('code.open_raw_file')"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
import type { CommandPaletteContextCommandInput } from '~/types/command-palette'
import { getPackageFileViewerUrl } from '#shared/utils/package-files'

// Maximum file size we'll try to load (500KB) - must match server
const MAX_FILE_SIZE = 500 * 1024
Expand Down Expand Up @@ -51,6 +52,9 @@ const packageName = computed(() => parsedRoute.value.packageName)
const version = computed(() => parsedRoute.value.version)
const filePathOrig = computed(() => parsedRoute.value.filePath)
const filePath = computed(() => parsedRoute.value.filePath?.replace(/\/$/, ''))
const rawFileUrl = computed(() =>
filePath.value ? getPackageFileViewerUrl(packageName.value, version.value, filePath.value) : '',
)

// Navigation helper - build URL for a path
function getCodeUrl(args: {
Expand Down Expand Up @@ -367,7 +371,7 @@ useCommandPaletteContextCommands(
label: $t('code.view_raw'),
keywords: [packageName.value, filePath.value],
iconClass: 'i-lucide:file-output',
href: `https://cdn.jsdelivr.net/npm/${packageName.value}@${version.value}/${filePath.value}`,
href: rawFileUrl.value,
})
}

Expand Down Expand Up @@ -552,10 +556,7 @@ onPrehydrate(el => {
})
}}
</p>
<LinkBase
variant="button-secondary"
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
>
<LinkBase variant="button-secondary" :to="rawFileUrl">
{{ $t('code.view_raw') }}
</LinkBase>
</div>
Expand All @@ -569,10 +570,7 @@ onPrehydrate(el => {
$t('code.file_size_warning', { size: bytesFormatter.format(currentNode?.size ?? 0) })
}}
</p>
<LinkBase
variant="button-secondary"
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
>
<LinkBase variant="button-secondary" :to="rawFileUrl">
{{ $t('code.view_raw') }}
</LinkBase>
</div>
Expand All @@ -582,10 +580,7 @@ onPrehydrate(el => {
<div class="i-lucide:circle-alert w-8 h-8 mx-auto text-fg-subtle mb-4" />
<p class="text-fg-muted mb-2">{{ $t('code.failed_to_load') }}</p>
<p class="text-fg-subtle text-sm mb-4">{{ $t('code.unavailable_hint') }}</p>
<LinkBase
variant="button-secondary"
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
>
<LinkBase variant="button-secondary" :to="rawFileUrl">
{{ $t('code.view_raw') }}
</LinkBase>
</div>
Expand Down
6 changes: 3 additions & 3 deletions modules/runtime/server/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,9 @@ async function handleJsdelivrDataApi(
name: parsed.name,
version: parsed.version || 'latest',
files: [
{ name: 'package.json', hash: 'abc123', size: 1000 },
{ name: 'index.js', hash: 'def456', size: 500 },
{ name: 'README.md', hash: 'ghi789', size: 2000 },
{ type: 'file', name: 'package.json', hash: 'abc123', size: 1000 },
{ type: 'file', name: 'index.js', hash: 'def456', size: 500 },
{ type: 'file', name: 'README.md', hash: 'ghi789', size: 2000 },
],
},
}
Expand Down
44 changes: 23 additions & 21 deletions server/api/registry/compare-file/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import * as v from 'valibot'
import {
fetchPackageFile,
PackageResponseTooLargeError,
readPackageResponseText,
} from '#server/utils/package-files'
import { PackageFileDiffQuerySchema } from '#shared/schemas/package'
import { countDiffStats, createDiff, insertSkipBlocks, truncateDiffHunks } from '#shared/utils/diff'
import type { DiffHunk, DiffSkipBlock } from '#shared/types/compare'
Expand Down Expand Up @@ -50,7 +55,7 @@ function countRenderableDiffBytes(hunks: (DiffHunk | DiffSkipBlock)[]): number {
}

/**
* Fetch file content from jsDelivr with size check
* Fetch package file content with a size check.
*/
async function fetchFileContentForDiff(
packageName: string,
Expand All @@ -59,42 +64,39 @@ async function fetchFileContentForDiff(
maxBytes: number,
signal?: AbortSignal,
): Promise<string | null> {
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), DIFF_TIMEOUT)
if (signal) {
signal.addEventListener('abort', () => controller.abort(signal.reason as any), { once: true })
}

try {
const response = await fetch(url, { signal: controller.signal })
const { provider, response } = await fetchPackageFile(
packageName,
version,
filePath,
controller.signal,
)

if (!response.ok) {
if (response.status === 404) return null
throw createError({
statusCode: response.status >= 500 ? 502 : response.status,
statusCode: provider === 'unpkg' || response.status >= 500 ? 502 : response.status,
message: `Failed to fetch file (${response.status})`,
})
}

const contentLength = response.headers.get('content-length')
if (contentLength && parseInt(contentLength, 10) > maxBytes) {
throw createError({
statusCode: 413,
message: `File too large to diff (${(parseInt(contentLength, 10) / 1024).toFixed(0)}KB). Maximum is ${maxBytes / 1024}KB.`,
})
}

const content = await response.text()

if (byteLength(content) > maxBytes) {
throw createError({
statusCode: 413,
message: `File too large to diff (${(byteLength(content) / 1024).toFixed(0)}KB). Maximum is ${maxBytes / 1024}KB.`,
})
try {
return await readPackageResponseText(response, maxBytes)
} catch (error) {
if (error instanceof PackageResponseTooLargeError) {
throw createError({
statusCode: 413,
message: `File too large to diff (${(error.sizeBytes / 1024).toFixed(0)}KB). Maximum is ${maxBytes / 1024}KB.`,
})
}
throw error
}

return content
} catch (error) {
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error
Expand Down
15 changes: 10 additions & 5 deletions server/api/registry/compare/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import * as v from 'valibot'
import { fetchPackageFile, readPackageResponseText } from '#server/utils/package-files'
import { PackageCompareQuerySchema } from '#shared/schemas/package'

const CACHE_VERSION = 1
const COMPARE_TIMEOUT = 8000 // 8 seconds
const MAX_PACKAGE_JSON_SIZE = 2 * 1024 * 1024

/**
* Fetch package.json from jsDelivr
* Fetch package.json from a package CDN.
*/
async function fetchPackageJson(
packageName: string,
version: string,
signal?: AbortSignal,
): Promise<Record<string, unknown> | null> {
try {
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/package.json`
const response = await fetch(url, { signal })
const { response } = await fetchPackageFile(packageName, version, 'package.json', signal)
if (!response.ok) return null
return (await response.json()) as Record<string, unknown>
} catch {
return JSON.parse(await readPackageResponseText(response, MAX_PACKAGE_JSON_SIZE)) as Record<
string,
unknown
>
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') throw error
return null
}
}
Expand Down
48 changes: 23 additions & 25 deletions server/api/registry/file/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as v from 'valibot'
import type { InternalImportsMap, PackageExportsMap } from '#server/utils/import-resolver'
import {
fetchPackageFile,
PackageResponseTooLargeError,
readPackageResponseText,
} from '#server/utils/package-files'
import { PackageFileQuerySchema } from '#shared/schemas/package'
import type { ReadmeResponse } from '#shared/types/readme'
import {
Expand All @@ -11,6 +16,7 @@ const CACHE_VERSION = 3

// Maximum file size to fetch and highlight (500KB)
const MAX_FILE_SIZE = 500 * 1024
const MAX_PACKAGE_JSON_SIZE = 2 * 1024 * 1024

// Languages that benefit from import linking
const IMPORT_LANGUAGES = new Set([
Expand All @@ -33,59 +39,51 @@ interface PackageJson {
}

/**
* Fetch package.json from jsDelivr to get dependency info
* Fetch package.json to get dependency info.
*/
async function fetchPackageJson(packageName: string, version: string): Promise<PackageJson | null> {
try {
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/package.json`
const response = await fetch(url)
const { response } = await fetchPackageFile(packageName, version, 'package.json')
if (!response.ok) return null
return (await response.json()) as PackageJson
return JSON.parse(await readPackageResponseText(response, MAX_PACKAGE_JSON_SIZE)) as PackageJson
} catch {
return null
}
}

/**
* Fetch file content from jsDelivr CDN.
* Fetch file content from a package CDN.
*/
async function fetchFileContent(
packageName: string,
version: string,
filePath: string,
): Promise<{ content: string; contentType: string | null }> {
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`
const response = await fetch(url)
const { response } = await fetchPackageFile(packageName, version, filePath)

if (!response.ok) {
if (response.status === 404) {
throw createError({ statusCode: 404, message: 'File not found' })
}
throw createError({
statusCode: 502,
message: 'Failed to fetch file from jsDelivr',
message: 'Failed to fetch package file',
})
}

const contentType = response.headers.get('content-type')

// Check content-length header if available
const contentLength = response.headers.get('content-length')
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
throw createError({
statusCode: 413,
message: `File too large (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Maximum size is ${MAX_FILE_SIZE / 1024}KB.`,
})
}

const content = await response.text()

// Double-check size after fetching (in case content-length wasn't set)
if (content.length > MAX_FILE_SIZE) {
throw createError({
statusCode: 413,
message: `File too large (${(content.length / 1024 / 1024).toFixed(1)}MB). Maximum size is ${MAX_FILE_SIZE / 1024}KB.`,
})
let content: string
try {
content = await readPackageResponseText(response, MAX_FILE_SIZE)
} catch (error) {
if (error instanceof PackageResponseTooLargeError) {
throw createError({
statusCode: 413,
message: `File exceeds the ${MAX_FILE_SIZE / 1024}KB maximum size.`,
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
throw error
}

return { content, contentType }
Expand Down
10 changes: 1 addition & 9 deletions server/api/registry/files/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,7 @@ export default defineCachedEventHandler(
version: rawVersion,
})

const jsDelivrData = await fetchFileTree(packageName, version)
const tree = convertToFileTree(jsDelivrData.files)

return {
package: packageName,
version,
default: jsDelivrData.default ?? undefined,
tree,
} satisfies PackageFileTreeResponse
return await getPackageFileTree(packageName, version)
} catch (error: unknown) {
handleApiError(error, {
statusCode: 502,
Expand Down
Loading
Loading