Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 8 additions & 4 deletions server/api/registry/compare/[...pkg].get.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
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>
return JSON.parse(await readPackageResponseText(response, MAX_PACKAGE_JSON_SIZE)) as Record<
string,
unknown
>
} catch {
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 too large (${(error.sizeBytes / 1024 / 1024).toFixed(1)}MB). Maximum size is ${MAX_FILE_SIZE / 1024}KB.`,
})
}
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