Skip to content

Commit c25cafc

Browse files
committed
wip
1 parent d85c65b commit c25cafc

File tree

4 files changed

+142
-2
lines changed

4 files changed

+142
-2
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as v from 'valibot'
2+
import type { InternalImportsMap } from '#server/utils/import-resolver'
23
import { PackageFileQuerySchema } from '#shared/schemas/package'
34
import type { ReadmeResponse } from '#shared/types/readme'
45
import {
@@ -27,6 +28,7 @@ interface PackageJson {
2728
devDependencies?: Record<string, string>
2829
peerDependencies?: Record<string, string>
2930
optionalDependencies?: Record<string, string>
31+
imports?: InternalImportsMap
3032
}
3133

3234
/**
@@ -159,7 +161,13 @@ export default defineCachedEventHandler(
159161
// Create resolver for relative imports
160162
if (fileTreeResponse) {
161163
const files = flattenFileTree(fileTreeResponse.tree)
162-
resolveRelative = createImportResolver(files, filePath, packageName, version)
164+
resolveRelative = createImportResolver(
165+
files,
166+
filePath,
167+
packageName,
168+
version,
169+
pkgJson?.imports,
170+
)
163171
}
164172
}
165173

@@ -200,6 +208,7 @@ export default defineCachedEventHandler(
200208
{
201209
// File content for a specific version never changes - cache permanently
202210
maxAge: CACHE_MAX_AGE_ONE_YEAR, // 1 year
211+
shouldBypassCache: () => import.meta.dev,
203212
getKey: event => {
204213
const pkg = getRouterParam(event, 'pkg') ?? ''
205214
return `file:v${CACHE_VERSION}:${pkg.replace(/\/+$/, '').trim()}`

server/utils/code-highlight.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ export function linkifyModuleSpecifiers(html: string, options?: LinkifyOptions):
178178
return resolveRelative(moduleSpecifier)
179179
}
180180

181+
if ((cleanSpec.startsWith('#') || cleanSpec.startsWith('~')) && resolveRelative) {
182+
return resolveRelative(moduleSpecifier)
183+
}
184+
181185
// Not a relative import - check if it's an npm package
182186
if (!isNpmPackage(moduleSpecifier)) {
183187
return null

server/utils/import-resolver.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export interface ResolvedImport {
133133
path: string
134134
}
135135

136+
export type InternalImportTarget = string | { default?: string; import?: string } | null | undefined
137+
138+
export type InternalImportsMap = Record<string, InternalImportTarget>
139+
136140
/**
137141
* Resolve a relative import specifier to an actual file path.
138142
*
@@ -200,6 +204,55 @@ export function resolveRelativeImport(
200204
return null
201205
}
202206

207+
function normalizeInternalImportTarget(target: InternalImportTarget): string | null {
208+
if (typeof target === 'string') {
209+
return target
210+
}
211+
212+
if (target && typeof target === 'object') {
213+
if (typeof target.import === 'string') {
214+
return target.import
215+
}
216+
217+
if (typeof target.default === 'string') {
218+
return target.default
219+
}
220+
}
221+
222+
return null
223+
}
224+
225+
/**
226+
* import ... from '#components/Button.vue'
227+
* import ... from '#/components/Button.vue'
228+
* import ... from '~/components/Button.vue'
229+
* import ... from '~components/Button.vue'
230+
*/
231+
export function resolveInternalImport(
232+
specifier: string,
233+
imports: InternalImportsMap | undefined,
234+
files: FileSet,
235+
): ResolvedImport | null {
236+
const cleanSpecifier = specifier.replace(/^['"]|['"]$/g, '').trim()
237+
238+
if ((!cleanSpecifier.startsWith('#') && !cleanSpecifier.startsWith('~')) || !imports) {
239+
return null
240+
}
241+
242+
const target = normalizeInternalImportTarget(imports[cleanSpecifier])
243+
console.log('resolved internal import', imports, cleanSpecifier, target)
244+
if (!target || !target.startsWith('./')) {
245+
return null
246+
}
247+
248+
const path = normalizePath(target)
249+
if (!path || path.startsWith('..') || !files.has(path)) {
250+
return null
251+
}
252+
253+
return { path }
254+
}
255+
203256
/**
204257
* Create a resolver function bound to a specific file tree and current file.
205258
*/
@@ -208,9 +261,13 @@ export function createImportResolver(
208261
currentFile: string,
209262
packageName: string,
210263
version: string,
264+
internalImports?: InternalImportsMap,
211265
): (specifier: string) => string | null {
212266
return (specifier: string) => {
213-
const resolved = resolveRelativeImport(specifier, currentFile, files)
267+
const relativeResolved = resolveRelativeImport(specifier, currentFile, files)
268+
const internalResolved = resolveInternalImport(specifier, internalImports, files)
269+
const resolved = relativeResolved ?? internalResolved
270+
214271
if (resolved) {
215272
return `/package-code/${packageName}/v/${version}/${resolved.path}`
216273
}

test/unit/server/utils/import-resolver.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PackageFileTree } from '../../../../shared/types'
33
import {
44
createImportResolver,
55
flattenFileTree,
6+
resolveInternalImport,
67
resolveRelativeImport,
78
} from '../../../../server/utils/import-resolver'
89

@@ -177,4 +178,73 @@ describe('createImportResolver', () => {
177178

178179
expect(url).toBe('/package-code/@scope/pkg/v/1.2.3/dist/utils.js')
179180
})
181+
182+
it('resolves package imports aliases to code browser URLs', () => {
183+
const files = new Set<string>(['dist/app/nuxt.js'])
184+
const resolver = createImportResolver(files, 'dist/index.js', 'nuxt', '4.3.1', {
185+
'#app/nuxt': './dist/app/nuxt.js',
186+
})
187+
188+
const url = resolver('#app/nuxt')
189+
190+
expect(url).toBe('/package-code/nuxt/v/4.3.1/dist/app/nuxt.js')
191+
})
192+
})
193+
194+
describe('resolveInternalSpecifier', () => {
195+
it('resolves exact imports map matches to files in the package', () => {
196+
const files = new Set<string>(['dist/app/nuxt.js'])
197+
198+
const resolved = resolveInternalImport(
199+
'#app/nuxt',
200+
{
201+
'#app/nuxt': './dist/app/nuxt.js',
202+
},
203+
files,
204+
)
205+
206+
expect(resolved?.path).toBe('dist/app/nuxt.js')
207+
})
208+
209+
it('supports import condition objects', () => {
210+
const files = new Set<string>(['dist/app/nuxt.js'])
211+
212+
const resolved = resolveInternalImport(
213+
'#app/nuxt',
214+
{
215+
'#app/nuxt': { import: './dist/app/nuxt.js' },
216+
},
217+
files,
218+
)
219+
220+
expect(resolved?.path).toBe('dist/app/nuxt.js')
221+
})
222+
223+
it('returns null when the target file does not exist', () => {
224+
const files = new Set<string>(['dist/app/index.js'])
225+
226+
const resolved = resolveInternalImport(
227+
'#app/nuxt',
228+
{
229+
'#app/nuxt': './dist/app/nuxt.js',
230+
},
231+
files,
232+
)
233+
234+
expect(resolved).toBeNull()
235+
})
236+
237+
it('returns null for unknown internal specifiers', () => {
238+
const files = new Set<string>(['dist/app/nuxt.js'])
239+
240+
const resolved = resolveInternalImport(
241+
'#app/nuxt',
242+
{
243+
'#app': './dist/app/index.js',
244+
},
245+
files,
246+
)
247+
248+
expect(resolved).toBeNull()
249+
})
180250
})

0 commit comments

Comments
 (0)