-
-
Notifications
You must be signed in to change notification settings - Fork 424
test: server utils #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1b0c626
5d9b1bb
b95715b
75fb1b5
94fb8f1
c2270d0
48bf88e
16811bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import type { JsDelivrFileNode, PackageFileTree } from '../../shared/types' | ||
| import { convertToFileTree, fetchFileTree, getPackageFileTree } from '../../server/utils/file-tree' | ||
|
|
||
| const getChildren = (node?: PackageFileTree): PackageFileTree[] => node?.children ?? [] | ||
|
|
||
| const mockFetchOk = <T>(body: T) => { | ||
| const fetchMock = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: async () => body, | ||
| }) | ||
| vi.stubGlobal('fetch', fetchMock) | ||
| return fetchMock | ||
| } | ||
|
|
||
| const mockFetchError = (status: number) => { | ||
| const fetchMock = vi.fn().mockResolvedValue({ | ||
| ok: false, | ||
| status, | ||
| }) | ||
| vi.stubGlobal('fetch', fetchMock) | ||
| return fetchMock | ||
| } | ||
|
|
||
|
Comment on lines
+7
to
+24
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could move these out for re-use but haven't looked at all the testing patterns in place yet in full |
||
| const mockCreateError = () => { | ||
| const createErrorMock = vi.fn((opts: { statusCode: number; message: string }) => opts) | ||
| vi.stubGlobal('createError', createErrorMock) | ||
| return createErrorMock | ||
| } | ||
|
|
||
| describe('convertToFileTree', () => { | ||
| it('converts jsDelivr nodes to a sorted tree with directories first', () => { | ||
| const input: JsDelivrFileNode[] = [ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not super familiar with jsDeliver. LMK if this is too rudimentary. |
||
| { type: 'file', name: 'zeta.txt', size: 120 }, | ||
| { | ||
| type: 'directory', | ||
| name: 'src', | ||
| files: [ | ||
| { type: 'file', name: 'b.ts', size: 5 }, | ||
| { type: 'file', name: 'a.ts', size: 3 }, | ||
| ], | ||
| }, | ||
| { type: 'file', name: 'alpha.txt', size: 10 }, | ||
| { | ||
| type: 'directory', | ||
| name: 'assets', | ||
| files: [{ type: 'file', name: 'logo.svg', size: 42 }], | ||
| }, | ||
| ] | ||
|
|
||
| const tree = convertToFileTree(input) | ||
|
|
||
| const names = tree.map(node => node.name) | ||
| expect(names).toEqual(['assets', 'src', 'alpha.txt', 'zeta.txt']) | ||
|
|
||
| const srcNode = tree.find(node => node.name === 'src') | ||
| expect(srcNode?.type).toBe('directory') | ||
| expect(getChildren(srcNode).map(child => child.name)).toEqual(['a.ts', 'b.ts']) | ||
| }) | ||
|
|
||
| it('builds correct paths and preserves file sizes', () => { | ||
| const input: JsDelivrFileNode[] = [ | ||
| { | ||
| type: 'directory', | ||
| name: 'src', | ||
| files: [ | ||
| { type: 'file', name: 'index.ts', size: 100 }, | ||
| { | ||
| type: 'directory', | ||
| name: 'utils', | ||
| files: [{ type: 'file', name: 'format.ts', size: 22 }], | ||
| }, | ||
| ], | ||
| }, | ||
| ] | ||
|
|
||
| const tree = convertToFileTree(input) | ||
|
|
||
| const src = tree[0] | ||
| expect(src?.path).toBe('src') | ||
|
|
||
| const indexFile = getChildren(src).find(child => child.name === 'index.ts') | ||
| expect(indexFile?.path).toBe('src/index.ts') | ||
| expect(indexFile?.size).toBe(100) | ||
|
|
||
| const utilsDir = getChildren(src).find(child => child.name === 'utils') | ||
| expect(utilsDir?.type).toBe('directory') | ||
|
|
||
| const formatFile = getChildren(utilsDir).find(child => child.name === 'format.ts') | ||
| expect(formatFile?.path).toBe('src/utils/format.ts') | ||
| expect(formatFile?.size).toBe(22) | ||
| }) | ||
|
|
||
| it('returns an empty tree for empty input', () => { | ||
| const tree = convertToFileTree([]) | ||
| const empty: PackageFileTree[] = [] | ||
| expect(tree).toEqual(empty) | ||
| }) | ||
|
|
||
| it('handles directories without a files property', () => { | ||
| const input: JsDelivrFileNode[] = [ | ||
| { | ||
| type: 'directory', | ||
| name: 'src', | ||
| }, | ||
| ] | ||
|
|
||
| const tree = convertToFileTree(input) | ||
|
|
||
| expect(tree[0]?.type).toBe('directory') | ||
| expect(tree[0]?.children).toEqual([]) | ||
| }) | ||
| }) | ||
|
|
||
| describe('fetchFileTree', () => { | ||
| it('returns parsed json when response is ok', async () => { | ||
| const body = { | ||
| type: 'npm', | ||
| name: 'pkg', | ||
| version: '1.0.0', | ||
| default: 'index.js', | ||
| files: [], | ||
| } | ||
|
|
||
| mockFetchOk(body) | ||
|
|
||
| try { | ||
| const result = await fetchFileTree('pkg', '1.0.0') | ||
| expect(result).toEqual(body) | ||
| } finally { | ||
| vi.unstubAllGlobals() | ||
| } | ||
| }) | ||
|
|
||
| it('throws a 404 error when package is not found', async () => { | ||
| mockFetchError(404) | ||
| mockCreateError() | ||
|
|
||
| try { | ||
| await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 404 }) | ||
| } finally { | ||
| vi.unstubAllGlobals() | ||
| } | ||
| }) | ||
|
|
||
| it('throws a 502 error for non-404 failures', async () => { | ||
| mockFetchError(500) | ||
| mockCreateError() | ||
|
|
||
| try { | ||
| await expect(fetchFileTree('pkg', '1.0.0')).rejects.toMatchObject({ statusCode: 502 }) | ||
| } finally { | ||
| vi.unstubAllGlobals() | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
| describe('getPackageFileTree', () => { | ||
| it('returns metadata and a converted tree', async () => { | ||
| const body = { | ||
| type: 'npm', | ||
| name: 'pkg', | ||
| version: '1.0.0', | ||
| default: 'index.js', | ||
| files: [ | ||
| { | ||
| type: 'directory', | ||
| name: 'src', | ||
| files: [{ type: 'file', name: 'index.js', size: 5 }], | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| mockFetchOk(body) | ||
|
|
||
| try { | ||
| const result = await getPackageFileTree('pkg', '1.0.0') | ||
| expect(result.package).toBe('pkg') | ||
| expect(result.version).toBe('1.0.0') | ||
| expect(result.default).toBe('index.js') | ||
| expect(result.tree[0]?.path).toBe('src') | ||
| expect(result.tree[0]?.children?.[0]?.path).toBe('src/index.js') | ||
| } finally { | ||
| vi.unstubAllGlobals() | ||
| } | ||
| }) | ||
|
|
||
| it('returns undefined when default is missing', async () => { | ||
| const body = { | ||
| type: 'npm', | ||
| name: 'pkg', | ||
| version: '1.0.0', | ||
| files: [], | ||
| } | ||
|
|
||
| mockFetchOk(body) | ||
|
|
||
| try { | ||
| const result = await getPackageFileTree('pkg', '1.0.0') | ||
| expect(result.default).toBeUndefined() | ||
| } finally { | ||
| vi.unstubAllGlobals() | ||
| } | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import type { PackageFileTree } from '../../shared/types' | ||
| import { | ||
| createImportResolver, | ||
| flattenFileTree, | ||
| resolveRelativeImport, | ||
| } from '../../server/utils/import-resolver' | ||
|
|
||
| describe('flattenFileTree', () => { | ||
| it('flattens nested trees into a file set', () => { | ||
| const tree: PackageFileTree[] = [ | ||
| { | ||
| name: 'dist', | ||
| path: 'dist', | ||
| type: 'directory', | ||
| children: [ | ||
| { name: 'index.js', path: 'dist/index.js', type: 'file', size: 10 }, | ||
| { | ||
| name: 'utils', | ||
| path: 'dist/utils', | ||
| type: 'directory', | ||
| children: [{ name: 'format.js', path: 'dist/utils/format.js', type: 'file', size: 5 }], | ||
| }, | ||
| ], | ||
| }, | ||
| ] | ||
|
|
||
| const files = flattenFileTree(tree) | ||
|
|
||
| expect(files.has('dist/index.js')).toBe(true) | ||
| expect(files.has('dist/utils/format.js')).toBe(true) | ||
| expect(files.has('dist/utils')).toBe(false) | ||
| }) | ||
|
|
||
| it('returns an empty set for an empty tree', () => { | ||
| const files = flattenFileTree([]) | ||
| expect(files.size).toBe(0) | ||
| }) | ||
|
|
||
| it('includes root-level files', () => { | ||
| const tree: PackageFileTree[] = [ | ||
| { name: 'index.js', path: 'index.js', type: 'file', size: 5 }, | ||
| { name: 'cli.js', path: 'cli.js', type: 'file', size: 3 }, | ||
| ] | ||
|
|
||
| const files = flattenFileTree(tree) | ||
|
|
||
| expect(files.has('index.js')).toBe(true) | ||
| expect(files.has('cli.js')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe('resolveRelativeImport', () => { | ||
| it('resolves a relative import with extension priority for JS files', () => { | ||
| const files = new Set<string>(['dist/utils.js', 'dist/utils.ts']) | ||
| const resolved = resolveRelativeImport('./utils', 'dist/index.js', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/utils.js') | ||
| }) | ||
|
|
||
| it('resolves a relative import with extension priority for TS files', () => { | ||
| const files = new Set<string>(['src/utils.ts', 'src/utils.js']) | ||
| const resolved = resolveRelativeImport('./utils', 'src/index.ts', files) | ||
|
|
||
| expect(resolved?.path).toBe('src/utils.ts') | ||
| }) | ||
|
|
||
| it('resolves a relative import to .d.ts when source is a declaration file', () => { | ||
| const files = new Set<string>(['dist/types.d.ts', 'dist/types.ts']) | ||
| const resolved = resolveRelativeImport('./types', 'dist/index.d.ts', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/types.d.ts') | ||
| }) | ||
|
|
||
| it('resolves an exact extension match', () => { | ||
| const files = new Set<string>(['src/utils.ts', 'src/utils.js']) | ||
| const resolved = resolveRelativeImport('./utils.ts', 'src/index.ts', files) | ||
|
|
||
| expect(resolved?.path).toBe('src/utils.ts') | ||
| }) | ||
|
|
||
| it('resolves a quoted specifier', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolved = resolveRelativeImport("'./utils'", 'dist/index.js', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/utils.js') | ||
| }) | ||
|
|
||
| it('resolves a relative import with extension priority for MTS files', () => { | ||
| const files = new Set<string>(['src/utils.mts', 'src/utils.mjs', 'src/utils.ts']) | ||
| const resolved = resolveRelativeImport('./utils', 'src/index.mts', files) | ||
|
|
||
| expect(resolved?.path).toBe('src/utils.mts') | ||
| }) | ||
|
|
||
| it('resolves a relative import with extension priority for MJS files', () => { | ||
| const files = new Set<string>(['dist/utils.mjs', 'dist/utils.js']) | ||
| const resolved = resolveRelativeImport('./utils', 'dist/index.mjs', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/utils.mjs') | ||
| }) | ||
|
|
||
| it('resolves a relative import with extension priority for CTS files', () => { | ||
| const files = new Set<string>(['src/utils.cts', 'src/utils.cjs', 'src/utils.ts']) | ||
| const resolved = resolveRelativeImport('./utils', 'src/index.cts', files) | ||
|
|
||
| expect(resolved?.path).toBe('src/utils.cts') | ||
| }) | ||
|
|
||
| it('resolves a relative import with extension priority for CJS files', () => { | ||
| const files = new Set<string>(['dist/utils.cjs', 'dist/utils.js']) | ||
| const resolved = resolveRelativeImport('./utils', 'dist/index.cjs', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/utils.cjs') | ||
| }) | ||
|
|
||
| it('resolves directory imports to index files', () => { | ||
| const files = new Set<string>(['dist/components/index.js']) | ||
| const resolved = resolveRelativeImport('./components', 'dist/index.js', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/components/index.js') | ||
| }) | ||
|
|
||
| it('resolves parent directory paths', () => { | ||
| const files = new Set<string>(['dist/shared/helpers.js']) | ||
| const resolved = resolveRelativeImport('../shared/helpers', 'dist/pages/home.js', files) | ||
|
|
||
| expect(resolved?.path).toBe('dist/shared/helpers.js') | ||
| }) | ||
|
|
||
| it('returns null when the path would go above the package root', () => { | ||
| const files = new Set<string>(['dist/index.js']) | ||
| const resolved = resolveRelativeImport('../../outside', 'dist/index.js', files) | ||
|
|
||
| expect(resolved).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null for non-relative imports', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolved = resolveRelativeImport('react', 'dist/index.js', files) | ||
|
|
||
| expect(resolved).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null when no matching file is found', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolved = resolveRelativeImport('./missing', 'dist/index.js', files) | ||
|
|
||
| expect(resolved).toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe('createImportResolver', () => { | ||
| it('creates a resolver that returns code browser URLs', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolver = createImportResolver(files, 'dist/index.js', 'pkg-name', '1.2.3') | ||
|
|
||
| const url = resolver('./utils') | ||
|
|
||
| expect(url).toBe('/code/pkg-name/v/1.2.3/dist/utils.js') | ||
| }) | ||
|
|
||
| it('returns null when the import cannot be resolved', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolver = createImportResolver(files, 'dist/index.js', 'pkg-name', '1.2.3') | ||
|
|
||
| const url = resolver('./missing') | ||
|
|
||
| expect(url).toBeNull() | ||
| }) | ||
|
|
||
| it('handles scoped package names in URLs', () => { | ||
| const files = new Set<string>(['dist/utils.js']) | ||
| const resolver = createImportResolver(files, 'dist/index.js', '@scope/pkg', '1.2.3') | ||
|
|
||
| const url = resolver('./utils') | ||
|
|
||
| expect(url).toBe('/code/@scope/pkg/v/1.2.3/dist/utils.js') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops ignore this change, will pull it out--meant to only keep it in this PR: #26
will pull it shortly