Skip to content

Commit 5cc31b6

Browse files
committed
feat: allow clicking to dependencies/relative paths in file tree
1 parent 3e14bbe commit 5cc31b6

9 files changed

Lines changed: 657 additions & 62 deletions

File tree

app/components/CodeViewer.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,24 @@ watch(
125125
transition: background-color 0.1s;
126126
}
127127
128-
/* Highlighted lines in code content */
128+
/* Highlighted lines in code content - extend full width with negative margin */
129129
.code-content :deep(.line.highlighted) {
130130
background: rgb(234 179 8 / 0.2); /* yellow-500/20 */
131131
margin: 0 -1rem;
132132
padding: 0 1rem;
133133
}
134+
135+
/* Clickable import links */
136+
.code-content :deep(.import-link) {
137+
color: inherit;
138+
text-decoration: underline;
139+
text-decoration-color: transparent;
140+
text-underline-offset: 2px;
141+
transition: text-decoration-color 0.15s;
142+
cursor: pointer;
143+
}
144+
145+
.code-content :deep(.import-link:hover) {
146+
text-decoration-color: currentColor;
147+
}
134148
</style>

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"nuxt-og-image": "^5.1.13",
3939
"perfect-debounce": "^2.1.0",
4040
"sanitize-html": "^2.17.0",
41+
"semver": "^7.7.3",
4142
"shiki": "^3.21.0",
4243
"ufo": "^1.6.3",
4344
"unplugin-vue-router": "^0.19.2",
@@ -50,6 +51,7 @@
5051
"@nuxt/test-utils": "3.23.0",
5152
"@playwright/test": "1.57.0",
5253
"@types/sanitize-html": "^2.16.0",
54+
"@types/semver": "^7.7.1",
5355
"@unocss/nuxt": "^66.6.0",
5456
"@unocss/preset-wind4": "^66.6.0",
5557
"@vite-pwa/assets-generator": "^1.0.2",

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
1+
const CACHE_VERSION = 2
2+
13
// Maximum file size to fetch and highlight (500KB)
24
const MAX_FILE_SIZE = 500 * 1024
35

6+
// Languages that benefit from import linking
7+
const IMPORT_LANGUAGES = new Set([
8+
'javascript', 'typescript', 'jsx', 'tsx',
9+
'vue', 'svelte', 'astro',
10+
])
11+
12+
interface PackageJson {
13+
dependencies?: Record<string, string>
14+
devDependencies?: Record<string, string>
15+
peerDependencies?: Record<string, string>
16+
optionalDependencies?: Record<string, string>
17+
}
18+
19+
/**
20+
* Fetch package.json from jsDelivr to get dependency info
21+
*/
22+
async function fetchPackageJson(packageName: string, version: string): Promise<PackageJson | null> {
23+
try {
24+
const url = `https://cdn.jsdelivr.net/npm/${packageName}@${version}/package.json`
25+
const response = await fetch(url)
26+
if (!response.ok) return null
27+
return await response.json() as PackageJson
28+
}
29+
catch {
30+
return null
31+
}
32+
}
33+
434
/**
535
* Fetch file content from jsDelivr CDN.
636
*/
@@ -73,7 +103,45 @@ export default defineCachedEventHandler(
73103
try {
74104
const content = await fetchFileContent(packageName, version, filePath)
75105
const language = getLanguageFromPath(filePath)
76-
const html = await highlightCode(content, language)
106+
107+
// For JS/TS files, resolve dependency versions and relative imports for linking
108+
let dependencies: Record<string, { version: string }> | undefined
109+
let resolveRelative: ((specifier: string) => string | null) | undefined
110+
111+
if (IMPORT_LANGUAGES.has(language)) {
112+
// Fetch package.json and file tree in parallel
113+
const [pkgJson, fileTreeResponse] = await Promise.all([
114+
fetchPackageJson(packageName, version),
115+
getPackageFileTree(packageName, version).catch(() => null),
116+
])
117+
118+
// Resolve npm dependency versions
119+
if (pkgJson) {
120+
// Merge all dependency types
121+
const allDeps: Record<string, string> = {
122+
...pkgJson.dependencies,
123+
...pkgJson.peerDependencies,
124+
...pkgJson.optionalDependencies,
125+
// Note: excluding devDependencies as they're less likely to be imported in dist files
126+
}
127+
128+
if (Object.keys(allDeps).length > 0) {
129+
const resolved: Record<string, string> = await resolveDependencyVersions(allDeps)
130+
dependencies = {}
131+
for (const [name, ver] of Object.entries(resolved)) {
132+
dependencies[name] = { version: ver }
133+
}
134+
}
135+
}
136+
137+
// Create resolver for relative imports
138+
if (fileTreeResponse) {
139+
const files = flattenFileTree(fileTreeResponse.tree)
140+
resolveRelative = createImportResolver(files, filePath, packageName, version)
141+
}
142+
}
143+
144+
const html = await highlightCode(content, language, { dependencies, resolveRelative })
77145

78146
return {
79147
package: packageName,
@@ -96,7 +164,7 @@ export default defineCachedEventHandler(
96164
maxAge: 60 * 60, // Cache for 1 hour (files don't change for a given version)
97165
getKey: (event) => {
98166
const pkg = getRouterParam(event, 'pkg') ?? ''
99-
return `file:${pkg}`
167+
return `file:v${CACHE_VERSION}:${pkg}`
100168
},
101169
},
102170
)

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

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,4 @@
1-
import type { JsDelivrPackageResponse, JsDelivrFileNode, PackageFileTree, PackageFileTreeResponse } from '#shared/types'
2-
3-
/**
4-
* Fetch the file tree from jsDelivr API.
5-
* Returns a nested tree structure of all files in the package.
6-
*/
7-
async function fetchFileTree(packageName: string, version: string): Promise<JsDelivrPackageResponse> {
8-
const url = `https://data.jsdelivr.com/v1/packages/npm/${packageName}@${version}`
9-
const response = await fetch(url)
10-
11-
if (!response.ok) {
12-
if (response.status === 404) {
13-
throw createError({ statusCode: 404, message: 'Package or version not found' })
14-
}
15-
throw createError({ statusCode: 502, message: 'Failed to fetch file list from jsDelivr' })
16-
}
17-
18-
return response.json()
19-
}
20-
21-
/**
22-
* Convert jsDelivr nested structure to our PackageFileTree format
23-
*/
24-
function convertToFileTree(nodes: JsDelivrFileNode[], parentPath: string = ''): PackageFileTree[] {
25-
const result: PackageFileTree[] = []
26-
27-
for (const node of nodes) {
28-
const path = parentPath ? `${parentPath}/${node.name}` : node.name
29-
30-
if (node.type === 'directory') {
31-
result.push({
32-
name: node.name,
33-
path,
34-
type: 'directory',
35-
children: node.files ? convertToFileTree(node.files, path) : [],
36-
})
37-
}
38-
else {
39-
result.push({
40-
name: node.name,
41-
path,
42-
type: 'file',
43-
size: node.size,
44-
})
45-
}
46-
}
47-
48-
// Sort: directories first, then files, alphabetically within each group
49-
result.sort((a, b) => {
50-
if (a.type !== b.type) {
51-
return a.type === 'directory' ? -1 : 1
52-
}
53-
return a.name.localeCompare(b.name)
54-
})
55-
56-
return result
57-
}
1+
import type { PackageFileTreeResponse } from '#shared/types'
582

593
/**
604
* Returns the file tree for a package version.

0 commit comments

Comments
 (0)