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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
PackageFileTreeResponse,
PackageFileContentResponse,
} from '#shared/types'
import { isBinaryFilePath } from '~/utils/file-types'

definePageMeta({
name: 'code',
Expand Down Expand Up @@ -107,7 +106,12 @@ const isViewingFile = computed(() => currentNode.value?.type === 'file')
// Maximum file size we'll try to load (500KB) - must match server
const MAX_FILE_SIZE = 500 * 1024

const isBinaryFile = computed(() => !!filePath.value && isBinaryFilePath(filePath.value))
// Estimate binary file based on mime type
const isBinaryFile = computed(() => {
const contentType = fileContent.value?.contentType
if (!contentType) return false
return isBinaryContentType(contentType)
})

const isFileTooLarge = computed(() => {
const size = currentNode.value?.size
Expand All @@ -117,13 +121,7 @@ const isFileTooLarge = computed(() => {
// Fetch file content when a file is selected (and not too large)
const fileContentUrl = computed(() => {
// Don't fetch if no file path, file tree not loaded, file is too large, or it's a directory
if (
!filePath.value ||
!fileTree.value ||
isFileTooLarge.value ||
!isViewingFile.value ||
isBinaryFile.value
) {
if (!filePath.value || !fileTree.value || isFileTooLarge.value || !isViewingFile.value) {
return null
}
return `/api/registry/file/${packageName.value}/v/${version.value}/${filePath.value}`
Expand Down Expand Up @@ -533,7 +531,13 @@ defineOgImageComponent('Default', {
<div v-else-if="isViewingFile && isBinaryFile" class="py-20 text-center">
<div class="i-lucide:binary w-12 h-12 mx-auto text-fg-subtle mb-4" />
<p class="text-fg-muted mb-2">{{ $t('code.binary_file') }}</p>
<p class="text-fg-subtle text-sm mb-4">{{ $t('code.binary_rendering_warning') }}</p>
<p class="text-fg-subtle text-sm mb-4">
{{
$t('code.binary_rendering_warning', {
contentType: fileContent?.contentType ?? 'unknown',
})
}}
</p>
<LinkBase
variant="button-secondary"
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
Expand Down
96 changes: 28 additions & 68 deletions app/utils/file-types.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,31 @@
// Extensions that are binary and cannot be meaningfully displayed as text
const BINARY_EXTENSIONS = new Set([
// Images
'png',
'jpg',
'jpeg',
'gif',
'webp',
'ico',
'bmp',
'tiff',
'tif',
'avif',
'heic',
'heif',
// Fonts
'woff',
'woff2',
'ttf',
'otf',
'eot',
// Archives
'zip',
'tar',
'gz',
'tgz',
'bz2',
'xz',
'7z',
'rar',
// Executables / compiled
'exe',
'dll',
'so',
'dylib',
'node',
'wasm',
'pyc',
'class',
// Media
'mp3',
'mp4',
'ogg',
'wav',
'avi',
'mov',
'webm',
'flac',
'aac',
'mkv',
// Documents
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
// Data
'bin',
'dat',
'db',
'sqlite',
'sqlite3',
// MIME types that are binary and cannot be meaningfully displayed as text
const BINARY_MIME_PREFIXES = new Set([
'image/',
'audio/',
'video/',
'font/',
'application/wasm',
'application/pdf',
'application/zip',
'application/x-rar-compressed',
'application/x-7z-compressed',
'application/x-gzip',
'application/x-tar',
'application/x-bz2',
'application/x-xz',
'application/x-executable',
'application/x-msdownload', // exe / dll
'application/x-sharedlib', // so / dylib
'application/msword', // .doc
'application/vnd.',
'application/octet-stream',
])

export function isBinaryFilePath(filePath: string): boolean {
const dotIndex = filePath.lastIndexOf('.')
const ext = dotIndex > -1 ? filePath.slice(dotIndex + 1).toLowerCase() : ''
return BINARY_EXTENSIONS.has(ext)
export function isBinaryContentType(contentType: string): boolean {
for (const prefix of BINARY_MIME_PREFIXES) {
if (contentType.startsWith(prefix)) {
return true
}
}
return false
}
2 changes: 1 addition & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@
"file_path": "File path",
"scroll_to_top": "Scroll to top",
"binary_file": "Binary file",
"binary_rendering_warning": "File type not supported for preview."
"binary_rendering_warning": "File type \"{contentType}\" is not supported for preview."
},
"badges": {
"provenance": {
Expand Down
9 changes: 6 additions & 3 deletions server/api/registry/file/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function fetchFileContent(
packageName: string,
version: string,
filePath: string,
): Promise<string> {
): Promise<{ content: string; contentType: string | null }> {
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`
const response = await fetch(url)

Expand All @@ -64,6 +64,8 @@ async function fetchFileContent(
})
}

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) {
Expand All @@ -83,7 +85,7 @@ async function fetchFileContent(
})
}

return content
return { content, contentType }
}

/**
Expand Down Expand Up @@ -123,7 +125,7 @@ export default defineCachedEventHandler(
filePath: rawFilePath,
})

const content = await fetchFileContent(packageName, version, filePath)
const { content, contentType } = await fetchFileContent(packageName, version, filePath)
const language = getLanguageFromPath(filePath)

// For JS/TS files, resolve dependency versions and relative imports for linking
Expand Down Expand Up @@ -185,6 +187,7 @@ export default defineCachedEventHandler(
version,
path: filePath,
language,
contentType,
content,
html,
lines: content.split('\n').length,
Expand Down
1 change: 1 addition & 0 deletions shared/types/npm-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export interface PackageFileContentResponse {
version: string
path: string
language: string
contentType: string | null
content: string
html: string
lines: number
Expand Down
Loading