diff --git a/test/search.ts b/test/search.ts new file mode 100644 index 0000000..59e7a78 --- /dev/null +++ b/test/search.ts @@ -0,0 +1,60 @@ +import { spawn } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' +import t from 'tap' + +const REGISTRY = 'https://registry.npmjs.org' + +t.test('search endpoint response matches SearchResponse', async (t) => { + const root = process.cwd() + const tsConfig = await readFile(join(root, 'tsconfig.json'), 'utf-8') + const searchResponse = await registrySearch('react') + + const dir = t.testdir({ + 'tsconfig-test.json': JSON.stringify({ + compilerOptions: { + ...JSON.parse(tsConfig).compilerOptions, + rootDir: 'fixtures', + }, + include: ['fixtures'], + }), + fixtures: { + 'search.ts': + `import type * as npmTypes from '../../../../types/index.d.ts'\n` + + `export const metadata: npmTypes.SearchResponse = ${JSON.stringify( + searchResponse, + (_k: string, v: unknown) => + (!Array.isArray(v) && v && typeof v === 'object') ? + Object.fromEntries( + Object.entries(v).sort(([a], [b]) => a.localeCompare(b)) + ) : v, + 2 + )}`, + }, + }) + + await new Promise((resolvePromise) => { + const proc = spawn( + resolve(root, './node_modules/.bin/tsc'), + ['--noEmit', '-p', './tsconfig-test.json'], + { cwd: dir } + ) + let output = '' + proc.stdout.on('data', (d) => (output += d.toString())) + proc.stderr.on('data', (d) => (output += d.toString())) + proc.on('close', (code) => { + t.equal(code, 0, output) + resolvePromise() + }) + }) +}) + +async function registrySearch (text: string) { + const res = await fetch(`${REGISTRY}/-/v1/search?text=${text}&size=1`) + + if (!res.ok) { + throw new Error(`Fetch failed: ${res.url} (status: ${res.status})`) + } + + return res.json() +} diff --git a/types/index.d.ts b/types/index.d.ts index ac4432d..7412a1b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -100,6 +100,75 @@ interface DevEngines { packageManager?: DevEngineDependency | DevEngineDependency[] } +interface SearchUser { + email?: string + username: string +} + +interface SearchTrustedPublisher { + oidcConfigId: string + id: string +} + +interface SearchPublisher extends SearchUser { + trustedPublisher?: SearchTrustedPublisher +} + +interface SearchPackageLinks { + bugs?: string + homepage?: string + npm?: string + repository?: string +} + +interface SearchPackage { + name: string + scope?: string + version: string + description?: string + keywords?: string[] + date: string + links: SearchPackageLinks + author?: SearchUser + publisher: SearchPublisher + maintainers: SearchUser[] + license?: string + sanitized_name: string +} + +interface SearchScore { + final: number + detail: { + popularity: number + quality: number + maintenance: number + } +} + +interface SearchFlags { + insecure?: 0 | 1 + unstable?: boolean +} + +export interface SearchResult { + package: SearchPackage + score: SearchScore + searchScore: number + downloads?: { + monthly?: number + weekly?: number + } + dependents?: string | number + updated?: string + flags?: SearchFlags +} + +export interface SearchResponse { + objects: SearchResult[] + total: number + time: string +} + // this is in the tarball for the project. it really could have anything in it. export interface PackageJSON { author?: Contact | string