Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions server/api/registry/analysis/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
} from '#shared/utils/constants'
import { parseRepoUrl } from '#shared/utils/git-providers'
import { encodePackageName } from '#shared/utils/npm'
import { flattenFileTree } from '#server/utils/import-resolver'
import { getPackageFileTree } from '#server/utils/file-tree'
import { getLatestVersion, getLatestVersionBatch } from 'fast-npm-meta'

interface AnalysisPackageJson extends ExtendedPackageJson {
Expand Down Expand Up @@ -50,18 +52,31 @@ export default defineCachedEventHandler(
`${NPM_REGISTRY}/${encodedName}${versionSuffix}`,
)

// Only check for @types package if the package doesn't ship its own types
let typesPackage: TypesPackageInfo | undefined
let files: Set<string> | undefined

// Only check for @types and files when the package doesn't ship its own types
if (!hasBuiltInTypes(pkg)) {
const typesPkgName = getTypesPackageName(packageName)
typesPackage = await fetchTypesPackageInfo(typesPkgName)

const resolvedVersion = pkg.version ?? version ?? 'latest'
try {
const fileTreeResponse = await getPackageFileTree(packageName, resolvedVersion)
files = flattenFileTree(fileTreeResponse.tree)
} catch {
// File tree fetch failed - skip implicit types check
}
}

// Check for associated create-* package (e.g., vite -> create-vite, next -> create-next-app)
// Only show if the packages are actually associated (same maintainers or same org)
const createPackage = await findAssociatedCreatePackage(packageName, pkg)

const analysis = analyzePackage(pkg, { typesPackage, createPackage })
const analysis = analyzePackage(pkg, {
typesPackage,
createPackage,
files,
})
const devDependencySuggestion = getDevDependencySuggestion(packageName, pkg.readme)

return {
Expand Down
18 changes: 16 additions & 2 deletions shared/utils/package-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,19 @@ export function getCreateShortName(createPackageName: string): string {
return createPackageName
}

function hasImplicitTypesInFiles(files: Set<string>): boolean {
return Array.from(files).some(
p => p.endsWith('.d.ts') || p.endsWith('.d.mts') || p.endsWith('.d.cts'),
)
}

/**
* Detect TypeScript types status for a package
*/
export function detectTypesStatus(
pkg: ExtendedPackageJson,
typesPackageInfo?: TypesPackageInfo,
files?: Set<string>,
): TypesStatus {
// Check for built-in types
if (pkg.types || pkg.typings) {
Expand All @@ -239,6 +246,12 @@ export function detectTypesStatus(
}
}

// Check for implicit types (e.g. .d.mts next to .mjs, TypeScript automatic lookup)
// Collect paths from exports/main/module and check if declaration files exist
if (files && hasImplicitTypesInFiles(files)) {
return { kind: 'included' }
}

// Check for @types package
if (typesPackageInfo) {
return {
Expand Down Expand Up @@ -289,6 +302,8 @@ export function getTypesPackageName(packageName: string): string {
export interface AnalyzePackageOptions {
typesPackage?: TypesPackageInfo
createPackage?: CreatePackageInfo
/** Flattened package file paths for implicit types detection (e.g. .d.mts next to .mjs) */
files?: Set<string>
}

/**
Expand All @@ -299,8 +314,7 @@ export function analyzePackage(
options?: AnalyzePackageOptions,
): PackageAnalysis {
const moduleFormat = detectModuleFormat(pkg)

const types = detectTypesStatus(pkg, options?.typesPackage)
const types = detectTypesStatus(pkg, options?.typesPackage, options?.files)

return {
moduleFormat,
Expand Down
58 changes: 58 additions & 0 deletions test/unit/shared/utils/package-analysis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,64 @@ describe('detectTypesStatus', () => {
it('returns none when no types detected', () => {
expect(detectTypesStatus({})).toEqual({ kind: 'none' })
})

it('detects included types when declaration file exists in files', () => {
expect(
detectTypesStatus(
{ type: 'module', exports: { '.': './dist/index.mjs' } },
undefined,
new Set(['dist/index.mjs', 'dist/index.d.mts']),
),
).toEqual({ kind: 'included' })
})
})

describe('detectTypesStatus implicit types (path derivation)', () => {
it('derives .d.mts from .mjs in exports', () => {
expect(
detectTypesStatus(
{ type: 'module', exports: { '.': './dist/index.mjs' } },
undefined,
new Set(['dist/index.d.mts']),
),
).toEqual({ kind: 'included' })
})

it('derives .d.cts from .cjs in exports', () => {
expect(
detectTypesStatus(
{ exports: { '.': { require: './dist/index.cjs' } } },
undefined,
new Set(['dist/index.d.cts']),
),
).toEqual({ kind: 'included' })
})

it('derives .d.mts from main when type is module', () => {
expect(
detectTypesStatus(
{ type: 'module', main: 'dist/index.mjs' },
undefined,
new Set(['dist/index.d.mts']),
),
).toEqual({ kind: 'included' })
})
})

describe('analyzePackage with files (implicit types)', () => {
it('detects included types when declaration file exists in files', () => {
const pkg = { type: 'module' as const, exports: { '.': './dist/index.mjs' } }
const files = new Set(['dist/index.mjs', 'dist/index.d.mts'])
const result = analyzePackage(pkg, { files })
expect(result.types).toEqual({ kind: 'included' })
})

it('returns none when declaration file does not exist in files', () => {
const pkg = { type: 'module' as const, exports: { '.': './dist/index.mjs' } }
const files = new Set(['dist/index.mjs'])
const result = analyzePackage(pkg, { files })
expect(result.types).toEqual({ kind: 'none' })
})
})

describe('getTypesPackageName', () => {
Expand Down
Loading