Skip to content

Commit 434619b

Browse files
feat: better algorithm for custom tsconfig paths
1 parent 2a2c0de commit 434619b

File tree

1 file changed

+149
-43
lines changed

1 file changed

+149
-43
lines changed

src/index.ts

+149-43
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ let cachedOptions: InternalResolverOptions | undefined
111111
let prevCwd: string
112112

113113
let mappersCachedOptions: InternalResolverOptions
114-
let mappers: Array<((specifier: string) => string[]) | null> | undefined
114+
let mappers: Array<{
115+
files: Set<string>
116+
mapperFn: NonNullable<ReturnType<typeof createPathsMatcher>>
117+
}> = []
115118

116119
let resolverCachedOptions: InternalResolverOptions
117120
let cachedResolver: Resolver | undefined
@@ -159,7 +162,7 @@ export function resolve(
159162
resolver = cachedResolver
160163
}
161164

162-
log('looking for:', source)
165+
log('looking for', source, 'in', file)
163166

164167
source = removeQuerystring(source)
165168

@@ -300,34 +303,35 @@ function getMappedPath(
300303
paths = [resolved]
301304
}
302305
} else {
303-
paths = mappers!
304-
.map(mapper =>
305-
mapper?.(source).map(item => [
306-
...extensions.map(ext => `${item}${ext}`),
307-
...originalExtensions.map(ext => `${item}/index${ext}`),
308-
]),
309-
)
310-
.flat(2)
311-
.filter(mappedPath => {
312-
if (mappedPath === undefined) {
313-
return false
314-
}
315-
316-
try {
317-
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false })
318-
if (stat === undefined) return false
319-
if (stat.isFile()) return true
320-
321-
// Maybe this is a module dir?
322-
if (stat.isDirectory()) {
323-
return isModule(mappedPath)
324-
}
325-
} catch {
326-
return false
306+
paths = [
307+
...new Set(
308+
mappers
309+
.filter(({ files }) => files.has(file))
310+
.map(({ mapperFn }) =>
311+
mapperFn(source).map(item => [
312+
...extensions.map(ext => `${item}${ext}`),
313+
...originalExtensions.map(ext => `${item}/index${ext}`),
314+
]),
315+
)
316+
.flat(2)
317+
.map(toNativePathSeparator),
318+
),
319+
].filter(mappedPath => {
320+
try {
321+
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false })
322+
if (stat === undefined) return false
323+
if (stat.isFile()) return true
324+
325+
// Maybe this is a module dir?
326+
if (stat.isDirectory()) {
327+
return isModule(mappedPath)
327328
}
328-
329+
} catch {
329330
return false
330-
})
331+
}
332+
333+
return false
334+
})
331335
}
332336

333337
if (retry && paths.length === 0) {
@@ -367,50 +371,109 @@ function getMappedPath(
367371
return paths[0]
368372
}
369373

374+
// eslint-disable-next-line sonarjs/cognitive-complexity
370375
function initMappers(options: InternalResolverOptions) {
371376
if (
372-
mappers &&
377+
mappers.length > 0 &&
373378
mappersCachedOptions === options &&
374379
prevCwd === process.cwd()
375380
) {
376381
return
377382
}
378383
prevCwd = process.cwd()
379384

380-
const configPaths =
385+
const configPaths = (
381386
typeof options.project === 'string'
382387
? [options.project]
383388
: Array.isArray(options.project)
384389
? options.project
385390
: [process.cwd()]
391+
) // 'tinyglobby' pattern must have POSIX separator
392+
.map(config => replacePathSeparator(config, path.sep, path.posix.sep))
386393

387-
const ignore = ['!**/node_modules/**']
394+
// https://github.com/microsoft/TypeScript/blob/df342b7206cb56b56bb3b3aecbb2ee2d2ff7b217/src/compiler/commandLineParser.ts#L3006
395+
const defaultInclude = ['**/*']
396+
const defaultIgnore = ['**/node_modules/**']
388397

389-
// turn glob patterns into paths
398+
// Turn glob patterns into paths
390399
const projectPaths = [
391400
...new Set([
392401
...configPaths.filter(path => !isDynamicPattern(path)),
393402
...globSync(
394-
[...configPaths.filter(path => isDynamicPattern(path)), ...ignore],
403+
configPaths.filter(path => isDynamicPattern(path)),
395404
{
396405
expandDirectories: false,
406+
ignore: defaultIgnore,
407+
absolute: true,
397408
},
398409
),
399410
]),
400411
]
401412

402-
mappers = projectPaths.map(projectPath => {
403-
let tsconfigResult: TsConfigResult | null
413+
mappers = projectPaths
414+
.map(projectPath => {
415+
let tsconfigResult: TsConfigResult | null
404416

405-
if (isFile(projectPath)) {
406-
const { dir, base } = path.parse(projectPath)
407-
tsconfigResult = getTsconfig(dir, base)
408-
} else {
409-
tsconfigResult = getTsconfig(projectPath)
410-
}
417+
if (isFile(projectPath)) {
418+
const { dir, base } = path.parse(projectPath)
419+
tsconfigResult = getTsconfig(dir, base)
420+
} else {
421+
tsconfigResult = getTsconfig(projectPath)
422+
}
411423

412-
return tsconfigResult && createPathsMatcher(tsconfigResult)
413-
})
424+
if (!tsconfigResult) {
425+
// eslint-disable-next-line unicorn/no-useless-undefined
426+
return undefined
427+
}
428+
429+
const mapperFn = createPathsMatcher(tsconfigResult)
430+
431+
if (!mapperFn) {
432+
// eslint-disable-next-line unicorn/no-useless-undefined
433+
return undefined
434+
}
435+
436+
const files =
437+
tsconfigResult.config.files === undefined &&
438+
tsconfigResult.config.include === undefined
439+
? // Include everything if no files or include options
440+
globSync(defaultInclude, {
441+
ignore: [
442+
...(tsconfigResult.config.exclude ?? []),
443+
...defaultIgnore,
444+
],
445+
absolute: true,
446+
cwd: path.dirname(tsconfigResult.path),
447+
})
448+
: [
449+
// https://www.typescriptlang.org/tsconfig/#files
450+
...(tsconfigResult.config.files !== undefined &&
451+
tsconfigResult.config.files.length > 0
452+
? tsconfigResult.config.files.map(file =>
453+
path.normalize(
454+
path.resolve(path.dirname(tsconfigResult!.path), file),
455+
),
456+
)
457+
: []),
458+
// https://www.typescriptlang.org/tsconfig/#include
459+
...(tsconfigResult.config.include !== undefined &&
460+
tsconfigResult.config.include.length > 0
461+
? globSync(tsconfigResult.config.include, {
462+
ignore: [
463+
...(tsconfigResult.config.exclude ?? []),
464+
...defaultIgnore,
465+
],
466+
absolute: true,
467+
})
468+
: []),
469+
]
470+
471+
return {
472+
files: new Set(files.map(toNativePathSeparator)),
473+
mapperFn,
474+
}
475+
})
476+
.filter(isDefined)
414477

415478
mappersCachedOptions = options
416479
}
@@ -427,3 +490,46 @@ function mangleScopedPackage(moduleName: string) {
427490
}
428491
return moduleName
429492
}
493+
494+
/**
495+
* Replace path `p` from `from` to `to` separator.
496+
*
497+
* @param {string} p Path
498+
* @param {typeof path.sep} from From separator
499+
* @param {typeof path.sep} to To separator
500+
* @returns Path with `to` separator
501+
*/
502+
function replacePathSeparator(
503+
p: string,
504+
from: typeof path.sep,
505+
to: typeof path.sep,
506+
) {
507+
return from === to ? p : p.replaceAll(from, to)
508+
}
509+
510+
/**
511+
* Replace path `p` separator to its native separator.
512+
*
513+
* @param {string} p Path
514+
* @returns Path with native separator
515+
*/
516+
function toNativePathSeparator(p: string) {
517+
return replacePathSeparator(
518+
p,
519+
path[process.platform === 'win32' ? 'posix' : 'win32'].sep,
520+
path[process.platform === 'win32' ? 'win32' : 'posix'].sep,
521+
)
522+
}
523+
524+
/**
525+
* Check if value is defined.
526+
*
527+
* Helper function for TypeScript.
528+
* Should be removed when upgrading to TypeScript >= 5.5.
529+
*
530+
* @param {T | null | undefined} value Value
531+
* @returns `true` if value is defined, `false` otherwise
532+
*/
533+
function isDefined<T>(value: T | null | undefined): value is T {
534+
return value !== null && value !== undefined
535+
}

0 commit comments

Comments
 (0)