diff --git a/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue b/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue index c7d74034aa..96e2ee5c15 100644 --- a/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue +++ b/app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue @@ -147,13 +147,13 @@ const currentNode = computed(() => { const isViewingFile = computed(() => currentNode.value?.type === 'file') -// Estimate binary file based on mime type +// Estimate binary file based on MIME type and the detected source language const isBinaryFile = computed(() => { if (!isViewingFile.value) return false const contentType = fileContent.value?.contentType if (!contentType) return false - return isBinaryContentType(contentType) + return isBinaryContentType(contentType, fileContent.value?.language) }) const isFileTooLarge = computed(() => { diff --git a/app/utils/file-types.ts b/app/utils/file-types.ts index cc3991b1c5..e198d71e12 100644 --- a/app/utils/file-types.ts +++ b/app/utils/file-types.ts @@ -21,7 +21,13 @@ const BINARY_MIME_PREFIXES = new Set([ 'application/octet-stream', ]) -export function isBinaryContentType(contentType: string): boolean { +export function isBinaryContentType(contentType: string, language?: string): boolean { + // CDNs use octet-stream for source formats they don't recognize. The server + // already identifies their language from the path; 'text' is its unknown fallback. + if (contentType.startsWith('application/octet-stream') && language && language !== 'text') { + return false + } + for (const prefix of BINARY_MIME_PREFIXES) { if (contentType.startsWith(prefix)) { return true diff --git a/server/api/registry/file/[...pkg].get.ts b/server/api/registry/file/[...pkg].get.ts index da250d9026..f2f4f92fa0 100644 --- a/server/api/registry/file/[...pkg].get.ts +++ b/server/api/registry/file/[...pkg].get.ts @@ -7,7 +7,7 @@ import { ERROR_PACKAGE_VERSION_AND_FILE_FAILED, } from '#shared/utils/constants' -const CACHE_VERSION = 3 +const CACHE_VERSION = 4 // Maximum file size to fetch and highlight (500KB) const MAX_FILE_SIZE = 500 * 1024 diff --git a/server/utils/code-highlight.ts b/server/utils/code-highlight.ts index e0eb6bb885..7af121fbe1 100644 --- a/server/utils/code-highlight.ts +++ b/server/utils/code-highlight.ts @@ -69,6 +69,7 @@ const FILENAME_MAP: Record = { 'package-lock.json': 'json', 'pnpm-lock.yaml': 'yaml', 'yarn.lock': 'yaml', + 'bun.lock': 'jsonc', 'Makefile': 'bash', 'Dockerfile': 'bash', 'LICENSE': 'text', diff --git a/test/unit/file-types.spec.ts b/test/unit/file-types.spec.ts new file mode 100644 index 0000000000..790d6e1f86 --- /dev/null +++ b/test/unit/file-types.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { isBinaryContentType } from '~/utils/file-types' + +describe('isBinaryContentType', () => { + it.each(['svelte', 'astro', 'typescript', 'jsonc'])( + 'previews recognized %s source served as octet-stream', + language => { + expect(isBinaryContentType('application/octet-stream', language)).toBe(false) + expect(isBinaryContentType('application/octet-stream; charset=utf-8', language)).toBe(false) + }, + ) + + it.each([undefined, '', 'text'])('keeps unknown octet-stream files binary (%s)', language => { + expect(isBinaryContentType('application/octet-stream', language)).toBe(true) + }) + + it.each(['image/svg+xml', 'application/wasm', 'application/zip', 'font/woff2'])( + 'keeps explicit binary MIME type %s binary even with a recognized language', + contentType => { + expect(isBinaryContentType(contentType, 'xml')).toBe(true) + }, + ) + + it.each(['text/plain', 'text/javascript', 'application/json'])( + 'previews text MIME type %s without a recognized language', + contentType => { + expect(isBinaryContentType(contentType)).toBe(false) + }, + ) +}) diff --git a/test/unit/server/utils/code-highlight.spec.ts b/test/unit/server/utils/code-highlight.spec.ts index 6408b13288..566fcaad49 100644 --- a/test/unit/server/utils/code-highlight.spec.ts +++ b/test/unit/server/utils/code-highlight.spec.ts @@ -173,6 +173,11 @@ describe('linkifyModuleSpecifiers', () => { }) describe('getLanguageFromPath', () => { + it('recognizes text bun lockfiles without treating binary lockfiles as text', () => { + expect(getLanguageFromPath('nested/bun.lock')).toBe('jsonc') + expect(getLanguageFromPath('nested/bun.lockb')).toBe('text') + }) + it('prefers well-known filenames over extension heuristics', () => { expect(getLanguageFromPath('foo/README.md')).toBe('markdown') expect(getLanguageFromPath('nested/tsconfig.json')).toBe('jsonc')