Skip to content

Commit 3e92bbb

Browse files
committed
fix: derive declaration paths from entry points instead of scanning all files
1 parent 08e6cba commit 3e92bbb

File tree

2 files changed

+139
-14
lines changed

2 files changed

+139
-14
lines changed

shared/utils/package-analysis.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,92 @@ export function getCreateShortName(createPackageName: string): string {
219219
return createPackageName
220220
}
221221

222-
function hasImplicitTypesInFiles(files: Set<string>): boolean {
223-
return Array.from(files).some(
224-
p => p.endsWith('.d.ts') || p.endsWith('.d.mts') || p.endsWith('.d.cts'),
225-
)
222+
/**
223+
* Map of JS extensions to their corresponding declaration file extensions.
224+
*/
225+
const DECLARATION_EXTENSIONS: Record<string, string[]> = {
226+
'.mjs': ['.d.mts', '.d.ts'],
227+
'.cjs': ['.d.cts', '.d.ts'],
228+
'.js': ['.d.ts', '.d.mts', '.d.cts'],
229+
}
230+
231+
/**
232+
* Collect concrete file paths from the exports field, skipping the "types"
233+
* condition (which is already checked by analyzeExports).
234+
*/
235+
function collectExportPaths(exports: PackageExports, depth = 0): string[] {
236+
if (depth > 10) return []
237+
if (exports === null || exports === undefined) return []
238+
239+
if (typeof exports === 'string') {
240+
return [exports]
241+
}
242+
243+
if (Array.isArray(exports)) {
244+
return exports.flatMap(item => collectExportPaths(item, depth + 1))
245+
}
246+
247+
if (typeof exports === 'object') {
248+
const paths: string[] = []
249+
for (const [key, value] of Object.entries(exports)) {
250+
// Skip "types" condition — already detected by analyzeExports
251+
if (key === 'types') continue
252+
paths.push(...collectExportPaths(value, depth + 1))
253+
}
254+
return paths
255+
}
256+
257+
return []
258+
}
259+
260+
/**
261+
* Normalize a path by stripping a leading "./" prefix.
262+
*/
263+
function stripRelativePrefix(p: string): string {
264+
return p.startsWith('./') ? p.slice(2) : p
265+
}
266+
267+
/**
268+
* Derive expected declaration file paths from a JS entry point path.
269+
* e.g. "./dist/index.mjs" -> ["dist/index.d.mts", "dist/index.d.ts"]
270+
*/
271+
function getDeclCandidates(entryPath: string): string[] {
272+
const normalized = stripRelativePrefix(entryPath)
273+
for (const [ext, declExts] of Object.entries(DECLARATION_EXTENSIONS)) {
274+
if (normalized.endsWith(ext)) {
275+
const base = normalized.slice(0, -ext.length)
276+
return declExts.map(de => base + de)
277+
}
278+
}
279+
return []
280+
}
281+
282+
/**
283+
* Check if declaration files exist for any of the package's entry points.
284+
* Derives expected declaration paths from exports/main/module entry points
285+
* (e.g. .d.mts for .mjs) and checks if they exist in the published files.
286+
*/
287+
function hasImplicitTypesForEntryPoints(pkg: ExtendedPackageJson, files: Set<string>): boolean {
288+
const entryPaths: string[] = []
289+
290+
if (pkg.exports) {
291+
entryPaths.push(...collectExportPaths(pkg.exports))
292+
}
293+
if (pkg.main) {
294+
entryPaths.push(pkg.main)
295+
}
296+
if (pkg.module) {
297+
entryPaths.push(pkg.module)
298+
}
299+
300+
for (const entryPath of entryPaths) {
301+
const candidates = getDeclCandidates(entryPath)
302+
if (candidates.some(c => files.has(c))) {
303+
return true
304+
}
305+
}
306+
307+
return false
226308
}
227309

228310
/**
@@ -246,9 +328,9 @@ export function detectTypesStatus(
246328
}
247329
}
248330

249-
// Check for implicit types (e.g. .d.mts next to .mjs, TypeScript automatic lookup)
250-
// Collect paths from exports/main/module and check if declaration files exist
251-
if (files && hasImplicitTypesInFiles(files)) {
331+
// Check for implicit types by deriving expected declaration file paths from
332+
// entry points (e.g. .d.mts for .mjs) and checking if they exist in the package
333+
if (files && hasImplicitTypesForEntryPoints(pkg, files)) {
252334
return { kind: 'included' }
253335
}
254336

test/unit/shared/utils/package-analysis.spec.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('detectTypesStatus', () => {
167167
expect(detectTypesStatus({})).toEqual({ kind: 'none' })
168168
})
169169

170-
it('detects included types when declaration file exists in files', () => {
170+
it('detects included types when matching declaration file exists for entry point', () => {
171171
expect(
172172
detectTypesStatus(
173173
{ type: 'module', exports: { '.': './dist/index.mjs' } },
@@ -176,10 +176,20 @@ describe('detectTypesStatus', () => {
176176
),
177177
).toEqual({ kind: 'included' })
178178
})
179+
180+
it('does not detect types from unrelated .d.ts files in the package', () => {
181+
expect(
182+
detectTypesStatus(
183+
{ type: 'module', exports: { '.': './dist/index.mjs' } },
184+
undefined,
185+
new Set(['dist/index.mjs', 'env.d.ts', 'shims-vue.d.ts']),
186+
),
187+
).toEqual({ kind: 'none' })
188+
})
179189
})
180190

181-
describe('detectTypesStatus implicit types (path derivation)', () => {
182-
it('derives .d.mts from .mjs in exports', () => {
191+
describe('detectTypesStatus implicit types from entry points', () => {
192+
it('finds .d.mts matching .mjs export entry point', () => {
183193
expect(
184194
detectTypesStatus(
185195
{ type: 'module', exports: { '.': './dist/index.mjs' } },
@@ -189,7 +199,7 @@ describe('detectTypesStatus implicit types (path derivation)', () => {
189199
).toEqual({ kind: 'included' })
190200
})
191201

192-
it('derives .d.cts from .cjs in exports', () => {
202+
it('finds .d.cts matching .cjs export entry point', () => {
193203
expect(
194204
detectTypesStatus(
195205
{ exports: { '.': { require: './dist/index.cjs' } } },
@@ -199,7 +209,17 @@ describe('detectTypesStatus implicit types (path derivation)', () => {
199209
).toEqual({ kind: 'included' })
200210
})
201211

202-
it('derives .d.mts from main when type is module', () => {
212+
it('finds .d.ts matching .js export entry point', () => {
213+
expect(
214+
detectTypesStatus(
215+
{ exports: { '.': './dist/index.js' } },
216+
undefined,
217+
new Set(['dist/index.d.ts']),
218+
),
219+
).toEqual({ kind: 'included' })
220+
})
221+
222+
it('finds .d.mts matching .mjs main entry point', () => {
203223
expect(
204224
detectTypesStatus(
205225
{ type: 'module', main: 'dist/index.mjs' },
@@ -208,22 +228,45 @@ describe('detectTypesStatus implicit types (path derivation)', () => {
208228
),
209229
).toEqual({ kind: 'included' })
210230
})
231+
232+
it('finds .d.ts matching .js module entry point', () => {
233+
expect(
234+
detectTypesStatus({ module: './dist/index.js' }, undefined, new Set(['dist/index.d.ts'])),
235+
).toEqual({ kind: 'included' })
236+
})
237+
238+
it('returns none when no declaration file matches any entry point', () => {
239+
expect(
240+
detectTypesStatus(
241+
{ type: 'module', exports: { '.': './dist/index.mjs' } },
242+
undefined,
243+
new Set(['dist/other.d.mts', 'types/env.d.ts']),
244+
),
245+
).toEqual({ kind: 'none' })
246+
})
211247
})
212248

213249
describe('analyzePackage with files (implicit types)', () => {
214-
it('detects included types when declaration file exists in files', () => {
250+
it('detects included types when matching declaration file exists for entry point', () => {
215251
const pkg = { type: 'module' as const, exports: { '.': './dist/index.mjs' } }
216252
const files = new Set(['dist/index.mjs', 'dist/index.d.mts'])
217253
const result = analyzePackage(pkg, { files })
218254
expect(result.types).toEqual({ kind: 'included' })
219255
})
220256

221-
it('returns none when declaration file does not exist in files', () => {
257+
it('returns none when no declaration file matches entry point', () => {
222258
const pkg = { type: 'module' as const, exports: { '.': './dist/index.mjs' } }
223259
const files = new Set(['dist/index.mjs'])
224260
const result = analyzePackage(pkg, { files })
225261
expect(result.types).toEqual({ kind: 'none' })
226262
})
263+
264+
it('returns none when only unrelated .d.ts files exist', () => {
265+
const pkg = { type: 'module' as const, exports: { '.': './dist/index.mjs' } }
266+
const files = new Set(['dist/index.mjs', 'env.d.ts'])
267+
const result = analyzePackage(pkg, { files })
268+
expect(result.types).toEqual({ kind: 'none' })
269+
})
227270
})
228271

229272
describe('getTypesPackageName', () => {

0 commit comments

Comments
 (0)