Skip to content

Commit d21b146

Browse files
authored
fix: use jsdelivr's content-type header to detect binary file (#2036)
1 parent 7062d19 commit d21b146

File tree

5 files changed

+50
-81
lines changed

5 files changed

+50
-81
lines changed

app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ const isViewingFile = computed(() => currentNode.value?.type === 'file')
100100
// Maximum file size we'll try to load (500KB) - must match server
101101
const MAX_FILE_SIZE = 500 * 1024
102102
103-
const isBinaryFile = computed(() => !!filePath.value && isBinaryFilePath(filePath.value))
103+
// Estimate binary file based on mime type
104+
const isBinaryFile = computed(() => {
105+
const contentType = fileContent.value?.contentType
106+
if (!contentType) return false
107+
return isBinaryContentType(contentType)
108+
})
104109
105110
const isFileTooLarge = computed(() => {
106111
const size = currentNode.value?.size
@@ -110,13 +115,7 @@ const isFileTooLarge = computed(() => {
110115
// Fetch file content when a file is selected (and not too large)
111116
const fileContentUrl = computed(() => {
112117
// Don't fetch if no file path, file tree not loaded, file is too large, or it's a directory
113-
if (
114-
!filePath.value ||
115-
!fileTree.value ||
116-
isFileTooLarge.value ||
117-
!isViewingFile.value ||
118-
isBinaryFile.value
119-
) {
118+
if (!filePath.value || !fileTree.value || isFileTooLarge.value || !isViewingFile.value) {
120119
return null
121120
}
122121
return `/api/registry/file/${packageName.value}/v/${version.value}/${filePath.value}`
@@ -481,7 +480,13 @@ defineOgImageComponent('Default', {
481480
<div v-else-if="isViewingFile && isBinaryFile" class="py-20 text-center">
482481
<div class="i-lucide:binary w-12 h-12 mx-auto text-fg-subtle mb-4" />
483482
<p class="text-fg-muted mb-2">{{ $t('code.binary_file') }}</p>
484-
<p class="text-fg-subtle text-sm mb-4">{{ $t('code.binary_rendering_warning') }}</p>
483+
<p class="text-fg-subtle text-sm mb-4">
484+
{{
485+
$t('code.binary_rendering_warning', {
486+
contentType: fileContent?.contentType ?? 'unknown',
487+
})
488+
}}
489+
</p>
485490
<LinkBase
486491
variant="button-secondary"
487492
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"

app/utils/file-types.ts

Lines changed: 28 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,31 @@
1-
// Extensions that are binary and cannot be meaningfully displayed as text
2-
const BINARY_EXTENSIONS = new Set([
3-
// Images
4-
'png',
5-
'jpg',
6-
'jpeg',
7-
'gif',
8-
'webp',
9-
'ico',
10-
'bmp',
11-
'tiff',
12-
'tif',
13-
'avif',
14-
'heic',
15-
'heif',
16-
// Fonts
17-
'woff',
18-
'woff2',
19-
'ttf',
20-
'otf',
21-
'eot',
22-
// Archives
23-
'zip',
24-
'tar',
25-
'gz',
26-
'tgz',
27-
'bz2',
28-
'xz',
29-
'7z',
30-
'rar',
31-
// Executables / compiled
32-
'exe',
33-
'dll',
34-
'so',
35-
'dylib',
36-
'node',
37-
'wasm',
38-
'pyc',
39-
'class',
40-
// Media
41-
'mp3',
42-
'mp4',
43-
'ogg',
44-
'wav',
45-
'avi',
46-
'mov',
47-
'webm',
48-
'flac',
49-
'aac',
50-
'mkv',
51-
// Documents
52-
'pdf',
53-
'doc',
54-
'docx',
55-
'xls',
56-
'xlsx',
57-
'ppt',
58-
'pptx',
59-
// Data
60-
'bin',
61-
'dat',
62-
'db',
63-
'sqlite',
64-
'sqlite3',
1+
// MIME types that are binary and cannot be meaningfully displayed as text
2+
const BINARY_MIME_PREFIXES = new Set([
3+
'image/',
4+
'audio/',
5+
'video/',
6+
'font/',
7+
'application/wasm',
8+
'application/pdf',
9+
'application/zip',
10+
'application/x-rar-compressed',
11+
'application/x-7z-compressed',
12+
'application/x-gzip',
13+
'application/x-tar',
14+
'application/x-bz2',
15+
'application/x-xz',
16+
'application/x-executable',
17+
'application/x-msdownload', // exe / dll
18+
'application/x-sharedlib', // so / dylib
19+
'application/msword', // .doc
20+
'application/vnd.',
21+
'application/octet-stream',
6522
])
6623

67-
export function isBinaryFilePath(filePath: string): boolean {
68-
const dotIndex = filePath.lastIndexOf('.')
69-
const ext = dotIndex > -1 ? filePath.slice(dotIndex + 1).toLowerCase() : ''
70-
return BINARY_EXTENSIONS.has(ext)
24+
export function isBinaryContentType(contentType: string): boolean {
25+
for (const prefix of BINARY_MIME_PREFIXES) {
26+
if (contentType.startsWith(prefix)) {
27+
return true
28+
}
29+
}
30+
return false
7131
}

i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@
793793
},
794794
"file_path": "File path",
795795
"binary_file": "Binary file",
796-
"binary_rendering_warning": "File type not supported for preview."
796+
"binary_rendering_warning": "File type \"{contentType}\" is not supported for preview."
797797
},
798798
"badges": {
799799
"provenance": {

server/api/registry/file/[...pkg].get.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async function fetchFileContent(
5050
packageName: string,
5151
version: string,
5252
filePath: string,
53-
): Promise<string> {
53+
): Promise<{ content: string; contentType: string | null }> {
5454
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`
5555
const response = await fetch(url)
5656

@@ -64,6 +64,8 @@ async function fetchFileContent(
6464
})
6565
}
6666

67+
const contentType = response.headers.get('content-type')
68+
6769
// Check content-length header if available
6870
const contentLength = response.headers.get('content-length')
6971
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
@@ -83,7 +85,7 @@ async function fetchFileContent(
8385
})
8486
}
8587

86-
return content
88+
return { content, contentType }
8789
}
8890

8991
/**
@@ -123,7 +125,7 @@ export default defineCachedEventHandler(
123125
filePath: rawFilePath,
124126
})
125127

126-
const content = await fetchFileContent(packageName, version, filePath)
128+
const { content, contentType } = await fetchFileContent(packageName, version, filePath)
127129
const language = getLanguageFromPath(filePath)
128130

129131
// For JS/TS files, resolve dependency versions and relative imports for linking
@@ -185,6 +187,7 @@ export default defineCachedEventHandler(
185187
version,
186188
path: filePath,
187189
language,
190+
contentType,
188191
content,
189192
html,
190193
lines: content.split('\n').length,

shared/types/npm-registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ export interface PackageFileContentResponse {
381381
version: string
382382
path: string
383383
language: string
384+
contentType: string | null
384385
content: string
385386
html: string
386387
lines: number

0 commit comments

Comments
 (0)