Skip to content

Commit c979fbf

Browse files
authored
test: server utils (#27)
1 parent 5b618d8 commit c979fbf

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

test/unit/file-tree.spec.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import type { JsDelivrFileNode, PackageFileTree } from '../../shared/types'
3+
import { convertToFileTree, fetchFileTree, getPackageFileTree } from '../../server/utils/file-tree'
4+
5+
const getChildren = (node?: PackageFileTree): PackageFileTree[] => node?.children ?? []
6+
7+
const mockFetchOk = <T>(body: T) => {
8+
const fetchMock = vi.fn().mockResolvedValue({
9+
ok: true,
10+
json: async () => body,
11+
})
12+
vi.stubGlobal('fetch', fetchMock)
13+
return fetchMock
14+
}
15+
16+
const mockFetchError = (status: number) => {
17+
const fetchMock = vi.fn().mockResolvedValue({
18+
ok: false,
19+
status,
20+
})
21+
vi.stubGlobal('fetch', fetchMock)
22+
return fetchMock
23+
}
24+
25+
const mockCreateError = () => {
26+
const createErrorMock = vi.fn((opts: { statusCode: number; message: string }) => opts)
27+
vi.stubGlobal('createError', createErrorMock)
28+
return createErrorMock
29+
}
30+
31+
describe('convertToFileTree', () => {
32+
it('converts jsDelivr nodes to a sorted tree with directories first', () => {
33+
const input: JsDelivrFileNode[] = [
34+
{ type: 'file', name: 'zeta.txt', size: 120 },
35+
{
36+
type: 'directory',
37+
name: 'src',
38+
files: [
39+
{ type: 'file', name: 'b.ts', size: 5 },
40+
{ type: 'file', name: 'a.ts', size: 3 },
41+
],
42+
},
43+
{ type: 'file', name: 'alpha.txt', size: 10 },
44+
{
45+
type: 'directory',
46+
name: 'assets',
47+
files: [{ type: 'file', name: 'logo.svg', size: 42 }],
48+
},
49+
]
50+
51+
const tree = convertToFileTree(input)
52+
53+
const names = tree.map(node => node.name)
54+
expect(names).toEqual(['assets', 'src', 'alpha.txt', 'zeta.txt'])
55+
56+
const srcNode = tree.find(node => node.name === 'src')
57+
expect(srcNode?.type).toBe('directory')
58+
expect(getChildren(srcNode).map(child => child.name)).toEqual(['a.ts', 'b.ts'])
59+
})
60+
61+
it('builds correct paths and preserves file sizes', () => {
62+
const input: JsDelivrFileNode[] = [
63+
{
64+
type: 'directory',
65+
name: 'src',
66+
files: [
67+
{ type: 'file', name: 'index.ts', size: 100 },
68+
{
69+
type: 'directory',
70+
name: 'utils',
71+
files: [{ type: 'file', name: 'format.ts', size: 22 }],
72+
},
73+
],
74+
},
75+
]
76+
77+
const tree = convertToFileTree(input)
78+
79+
const src = tree[0]
80+
expect(src?.path).toBe('src')
81+
82+
const indexFile = getChildren(src).find(child => child.name === 'index.ts')
83+
expect(indexFile?.path).toBe('src/index.ts')
84+
expect(indexFile?.size).toBe(100)
85+
86+
const utilsDir = getChildren(src).find(child => child.name === 'utils')
87+
expect(utilsDir?.type).toBe('directory')
88+
89+
const formatFile = getChildren(utilsDir).find(child => child.name === 'format.ts')
90+
expect(formatFile?.path).toBe('src/utils/format.ts')
91+
expect(formatFile?.size).toBe(22)
92+
})
93+
94+
it('returns an empty tree for empty input', () => {
95+
const tree = convertToFileTree([])
96+
const empty: PackageFileTree[] = []
97+
expect(tree).toEqual(empty)
98+
})
99+
100+
it('handles directories without a files property', () => {
101+
const input: JsDelivrFileNode[] = [
102+
{
103+
type: 'directory',
104+
name: 'src',
105+
},
106+
]
107+
108+
const tree = convertToFileTree(input)
109+
110+
expect(tree[0]?.type).toBe('directory')
111+
expect(tree[0]?.children).toEqual([])
112+
})
113+
})
114+
115+
describe('fetchFileTree', () => {
116+
it('returns parsed json when response is ok', async () => {
117+
const body = {
118+
type: 'npm',
119+
name: 'pkg',
120+
version: '1.0.0',
121+
default: 'index.js',
122+
files: [],
123+
}
124+
125+
mockFetchOk(body)
126+
127+
try {
128+
const result = await fetchFileTree('pkg', '1.0.0')
129+
expect(result).toEqual(body)
130+
} finally {
131+
vi.unstubAllGlobals()
132+
}
133+
})
134+
135+
it('throws a 404 error when package is not found', async () => {
136+
mockFetchError(404)
137+
mockCreateError()
138+
139+
try {
140+
await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 404 })
141+
} finally {
142+
vi.unstubAllGlobals()
143+
}
144+
})
145+
146+
it('throws a 502 error for non-404 failures', async () => {
147+
mockFetchError(500)
148+
mockCreateError()
149+
150+
try {
151+
await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 502 })
152+
} finally {
153+
vi.unstubAllGlobals()
154+
}
155+
})
156+
})
157+
158+
describe('getPackageFileTree', () => {
159+
it('returns metadata and a converted tree', async () => {
160+
const body = {
161+
type: 'npm',
162+
name: 'pkg',
163+
version: '1.0.0',
164+
default: 'index.js',
165+
files: [
166+
{
167+
type: 'directory',
168+
name: 'src',
169+
files: [{ type: 'file', name: 'index.js', size: 5 }],
170+
},
171+
],
172+
}
173+
174+
mockFetchOk(body)
175+
176+
try {
177+
const result = await getPackageFileTree('pkg', '1.0.0')
178+
expect(result.package).toBe('pkg')
179+
expect(result.version).toBe('1.0.0')
180+
expect(result.default).toBe('index.js')
181+
expect(result.tree[0]?.path).toBe('src')
182+
expect(result.tree[0]?.children?.[0]?.path).toBe('src/index.js')
183+
} finally {
184+
vi.unstubAllGlobals()
185+
}
186+
})
187+
188+
it('returns undefined when default is missing', async () => {
189+
const body = {
190+
type: 'npm',
191+
name: 'pkg',
192+
version: '1.0.0',
193+
files: [],
194+
}
195+
196+
mockFetchOk(body)
197+
198+
try {
199+
const result = await getPackageFileTree('pkg', '1.0.0')
200+
expect(result.default).toBeUndefined()
201+
} finally {
202+
vi.unstubAllGlobals()
203+
}
204+
})
205+
})

test/unit/import-resolver.spec.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { PackageFileTree } from '../../shared/types'
3+
import {
4+
createImportResolver,
5+
flattenFileTree,
6+
resolveRelativeImport,
7+
} from '../../server/utils/import-resolver'
8+
9+
describe('flattenFileTree', () => {
10+
it('flattens nested trees into a file set', () => {
11+
const tree: PackageFileTree[] = [
12+
{
13+
name: 'dist',
14+
path: 'dist',
15+
type: 'directory',
16+
children: [
17+
{ name: 'index.js', path: 'dist/index.js', type: 'file', size: 10 },
18+
{
19+
name: 'utils',
20+
path: 'dist/utils',
21+
type: 'directory',
22+
children: [{ name: 'format.js', path: 'dist/utils/format.js', type: 'file', size: 5 }],
23+
},
24+
],
25+
},
26+
]
27+
28+
const files = flattenFileTree(tree)
29+
30+
expect(files.has('dist/index.js')).toBe(true)
31+
expect(files.has('dist/utils/format.js')).toBe(true)
32+
expect(files.has('dist/utils')).toBe(false)
33+
})
34+
35+
it('returns an empty set for an empty tree', () => {
36+
const files = flattenFileTree([])
37+
expect(files.size).toBe(0)
38+
})
39+
40+
it('includes root-level files', () => {
41+
const tree: PackageFileTree[] = [
42+
{ name: 'index.js', path: 'index.js', type: 'file', size: 5 },
43+
{ name: 'cli.js', path: 'cli.js', type: 'file', size: 3 },
44+
]
45+
46+
const files = flattenFileTree(tree)
47+
48+
expect(files.has('index.js')).toBe(true)
49+
expect(files.has('cli.js')).toBe(true)
50+
})
51+
})
52+
53+
describe('resolveRelativeImport', () => {
54+
it('resolves a relative import with extension priority for JS files', () => {
55+
const files = new Set<string>(['dist/utils.js', 'dist/utils.ts'])
56+
const resolved = resolveRelativeImport('./utils', 'dist/index.js', files)
57+
58+
expect(resolved?.path).toBe('dist/utils.js')
59+
})
60+
61+
it('resolves a relative import with extension priority for TS files', () => {
62+
const files = new Set<string>(['src/utils.ts', 'src/utils.js'])
63+
const resolved = resolveRelativeImport('./utils', 'src/index.ts', files)
64+
65+
expect(resolved?.path).toBe('src/utils.ts')
66+
})
67+
68+
it('resolves a relative import to .d.ts when source is a declaration file', () => {
69+
const files = new Set<string>(['dist/types.d.ts', 'dist/types.ts'])
70+
const resolved = resolveRelativeImport('./types', 'dist/index.d.ts', files)
71+
72+
expect(resolved?.path).toBe('dist/types.d.ts')
73+
})
74+
75+
it('resolves an exact extension match', () => {
76+
const files = new Set<string>(['src/utils.ts', 'src/utils.js'])
77+
const resolved = resolveRelativeImport('./utils.ts', 'src/index.ts', files)
78+
79+
expect(resolved?.path).toBe('src/utils.ts')
80+
})
81+
82+
it('resolves a quoted specifier', () => {
83+
const files = new Set<string>(['dist/utils.js'])
84+
const resolved = resolveRelativeImport("'./utils'", 'dist/index.js', files)
85+
86+
expect(resolved?.path).toBe('dist/utils.js')
87+
})
88+
89+
it('resolves a relative import with extension priority for MTS files', () => {
90+
const files = new Set<string>(['src/utils.mts', 'src/utils.mjs', 'src/utils.ts'])
91+
const resolved = resolveRelativeImport('./utils', 'src/index.mts', files)
92+
93+
expect(resolved?.path).toBe('src/utils.mts')
94+
})
95+
96+
it('resolves a relative import with extension priority for MJS files', () => {
97+
const files = new Set<string>(['dist/utils.mjs', 'dist/utils.js'])
98+
const resolved = resolveRelativeImport('./utils', 'dist/index.mjs', files)
99+
100+
expect(resolved?.path).toBe('dist/utils.mjs')
101+
})
102+
103+
it('resolves a relative import with extension priority for CTS files', () => {
104+
const files = new Set<string>(['src/utils.cts', 'src/utils.cjs', 'src/utils.ts'])
105+
const resolved = resolveRelativeImport('./utils', 'src/index.cts', files)
106+
107+
expect(resolved?.path).toBe('src/utils.cts')
108+
})
109+
110+
it('resolves a relative import with extension priority for CJS files', () => {
111+
const files = new Set<string>(['dist/utils.cjs', 'dist/utils.js'])
112+
const resolved = resolveRelativeImport('./utils', 'dist/index.cjs', files)
113+
114+
expect(resolved?.path).toBe('dist/utils.cjs')
115+
})
116+
117+
it('resolves directory imports to index files', () => {
118+
const files = new Set<string>(['dist/components/index.js'])
119+
const resolved = resolveRelativeImport('./components', 'dist/index.js', files)
120+
121+
expect(resolved?.path).toBe('dist/components/index.js')
122+
})
123+
124+
it('resolves parent directory paths', () => {
125+
const files = new Set<string>(['dist/shared/helpers.js'])
126+
const resolved = resolveRelativeImport('../shared/helpers', 'dist/pages/home.js', files)
127+
128+
expect(resolved?.path).toBe('dist/shared/helpers.js')
129+
})
130+
131+
it('returns null when the path would go above the package root', () => {
132+
const files = new Set<string>(['dist/index.js'])
133+
const resolved = resolveRelativeImport('../../outside', 'dist/index.js', files)
134+
135+
expect(resolved).toBeNull()
136+
})
137+
138+
it('returns null for non-relative imports', () => {
139+
const files = new Set<string>(['dist/utils.js'])
140+
const resolved = resolveRelativeImport('react', 'dist/index.js', files)
141+
142+
expect(resolved).toBeNull()
143+
})
144+
145+
it('returns null when no matching file is found', () => {
146+
const files = new Set<string>(['dist/utils.js'])
147+
const resolved = resolveRelativeImport('./missing', 'dist/index.js', files)
148+
149+
expect(resolved).toBeNull()
150+
})
151+
})
152+
153+
describe('createImportResolver', () => {
154+
it('creates a resolver that returns code browser URLs', () => {
155+
const files = new Set<string>(['dist/utils.js'])
156+
const resolver = createImportResolver(files, 'dist/index.js', 'pkg-name', '1.2.3')
157+
158+
const url = resolver('./utils')
159+
160+
expect(url).toBe('/code/pkg-name/v/1.2.3/dist/utils.js')
161+
})
162+
163+
it('returns null when the import cannot be resolved', () => {
164+
const files = new Set<string>(['dist/utils.js'])
165+
const resolver = createImportResolver(files, 'dist/index.js', 'pkg-name', '1.2.3')
166+
167+
const url = resolver('./missing')
168+
169+
expect(url).toBeNull()
170+
})
171+
172+
it('handles scoped package names in URLs', () => {
173+
const files = new Set<string>(['dist/utils.js'])
174+
const resolver = createImportResolver(files, 'dist/index.js', '@scope/pkg', '1.2.3')
175+
176+
const url = resolver('./utils')
177+
178+
expect(url).toBe('/code/@scope/pkg/v/1.2.3/dist/utils.js')
179+
})
180+
})

0 commit comments

Comments
 (0)