Skip to content

Commit

Permalink
feat: look up original TS sources when dealing with folders as modules
Browse files Browse the repository at this point in the history
  • Loading branch information
sorccu committed Dec 19, 2024
1 parent 04bda04 commit 5ce630d
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ export class PackageJsonFile {

jsonFile: JsonSourceFile<Schema>
basePath: string
mainPaths: string[]

private constructor (jsonFile: JsonSourceFile<Schema>) {
this.jsonFile = jsonFile
this.basePath = jsonFile.meta.dirname

const fallbackMainPath = path.resolve(this.basePath, 'index.js')

this.mainPaths = jsonFile.data.main !== undefined
? [path.resolve(this.basePath, jsonFile.data.main), fallbackMainPath]
: [fallbackMainPath]
}

public get meta () {
Expand Down Expand Up @@ -55,8 +62,4 @@ export class PackageJsonFile {
supportsPackageRelativePaths () {
return this.jsonFile.data.exports === undefined
}

mainPath () {
return path.resolve(this.meta.dirname, this.jsonFile.data.main ?? 'index.js')
}
}
95 changes: 68 additions & 27 deletions packages/cli/src/services/check-parser/package-files/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class PackageFilesResolver {
packageJsonCache = new FileLoader(PackageJsonFile.loadFromFilePath)
tsconfigJsonCache = new FileLoader(TSConfigFile.loadFromFilePath)

loadPackageFiles (filePath: string): PackageFiles {
loadPackageFiles (filePath: string, options?: { root?: string }): PackageFiles {
const files: PackageFiles = {}

let currentPath = filePath
Expand All @@ -61,24 +61,53 @@ export class PackageFilesResolver {
if (files.packageJson !== undefined && files.tsconfigJson !== undefined) {
break
}

// Stop if we reach the user-specified root directory.
// TODO: I don't like a string comparison for this but it'll do for now.
if (currentPath === options?.root) {
break
}
}

return files
}

private resolveSourceFile (sourceFile: SourceFile): SourceFile {
private resolveSourceFile (sourceFile: SourceFile): SourceFile | undefined {
if (sourceFile.meta.basename === PackageJsonFile.FILENAME) {
const packageJson = this.packageJsonCache.load(sourceFile.meta.filePath)
if (packageJson === undefined) {
return sourceFile
}

const mainSourceFile = SourceFile.loadFromFilePath(packageJson.mainPath())
if (mainSourceFile === undefined) {
return sourceFile
// Go through each main path. A fallback path is included. If we can
// find a tsconfig for the main file, look it up and attempt to find
// the original TypeScript sources roughly the same way tsc does it.
for (const mainPath of packageJson.mainPaths) {
const { tsconfigJson } = this.loadPackageFiles(mainPath, {
root: packageJson.basePath,
})

if (tsconfigJson === undefined) {
const mainSourceFile = SourceFile.loadFromFilePath(mainPath)
if (mainSourceFile === undefined) {
continue
}

return mainSourceFile
}

const candidatePaths = tsconfigJson.collectLookupPaths(mainPath)
for (const candidatePath of candidatePaths) {
const mainSourceFile = SourceFile.loadFromFilePath(candidatePath)
if (mainSourceFile === undefined) {
continue
}

return mainSourceFile
}
}

return mainSourceFile
return undefined
}

return sourceFile
Expand All @@ -104,11 +133,14 @@ export class PackageFilesResolver {
const relativeDepPath = path.resolve(dirname, dep)
const sourceFile = SourceFile.loadFromFilePath(relativeDepPath, suffixes)
if (sourceFile !== undefined) {
resolved.local.push({
sourceFile: this.resolveSourceFile(sourceFile),
origin: 'relative-path',
})
continue
const resolvedFile = this.resolveSourceFile(sourceFile)
if (resolvedFile !== undefined) {
resolved.local.push({
sourceFile: resolvedFile,
origin: 'relative-path',
})
continue
}
}
resolved.missing.push({
spec: dep,
Expand All @@ -125,12 +157,15 @@ export class PackageFilesResolver {
const relativePath = path.resolve(tsconfigJson.basePath, resolvedPath)
const sourceFile = SourceFile.loadFromFilePath(relativePath, suffixes)
if (sourceFile !== undefined) {
resolved.local.push({
sourceFile: this.resolveSourceFile(sourceFile),
origin: 'tsconfig-resolved-path',
})
found = true
break // We only need the first match that exists.
const resolvedFile = this.resolveSourceFile(sourceFile)
if (resolvedFile !== undefined) {
resolved.local.push({
sourceFile: resolvedFile,
origin: 'tsconfig-resolved-path',
})
found = true
break // We only need the first match that exists.
}
}
}
if (found) {
Expand All @@ -142,11 +177,14 @@ export class PackageFilesResolver {
const relativePath = path.resolve(tsconfigJson.basePath, tsconfigJson.baseUrl, dep)
const sourceFile = SourceFile.loadFromFilePath(relativePath, suffixes)
if (sourceFile !== undefined) {
resolved.local.push({
sourceFile: this.resolveSourceFile(sourceFile),
origin: 'tsconfig-baseurl-relative-path',
})
continue
const resolvedFile = this.resolveSourceFile(sourceFile)
if (resolvedFile !== undefined) {
resolved.local.push({
sourceFile: resolvedFile,
origin: 'tsconfig-baseurl-relative-path',
})
continue
}
}
}
}
Expand All @@ -156,11 +194,14 @@ export class PackageFilesResolver {
const relativePath = path.resolve(packageJson.basePath, dep)
const sourceFile = SourceFile.loadFromFilePath(relativePath, suffixes)
if (sourceFile !== undefined) {
resolved.local.push({
sourceFile: this.resolveSourceFile(sourceFile),
origin: 'package-relative-path',
})
continue
const resolvedFile = this.resolveSourceFile(sourceFile)
if (resolvedFile !== undefined) {
resolved.local.push({
sourceFile: resolvedFile,
origin: 'package-relative-path',
})
continue
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,42 @@ interface CompilerOptions {
moduleResolution?: ModuleResolution
baseUrl?: string
paths?: Paths

/**
* If set, .js files will be emitted into this directory.
*
* If not set, .js files are placed right next to .ts files in the same
* folder.
*
* @see https://www.typescriptlang.org/tsconfig/#outDir
*/
outDir?: string

/**
* If not set, rootDir is inferred to be the longest common path of all
* non-declaration input files, unless `composite: true`, in which case
* the inferred root is the directory containing the tsconfig.json file.
*
* @see https://www.typescriptlang.org/tsconfig/#rootDir
*/
rootDir?: string

/**
* Allow multiple directions to act as a single root. Source files will be
* able to refer to files in other roots as if they were present in the same
* root.
*
* @see https://www.typescriptlang.org/tsconfig/#rootDirs
*/
rootDirs?: string[]

/**
* If true, the default rootDir is the directory containing the
* tsconfig.json file.
*
* @see https://www.typescriptlang.org/tsconfig/#composite
*/
composite?: boolean
}

export interface Schema {
Expand All @@ -28,6 +64,23 @@ export type Options = {
jsonSourceFileLoader?: LoadFile<JsonSourceFile<Schema>>,
}

type JSExtension = '.js' | '.mjs' | '.cjs'

const JSExtensions: JSExtension[] = ['.js', '.mjs', '.cjs']

type JSExtensionMappings = {
[key in JSExtension]: string[]
}

/**
* @see https://www.typescriptlang.org/docs/handbook/modules/reference.html#file-extension-substitution
*/
const extensionMappings: JSExtensionMappings = {
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
'.cjs': ['.cts', '.cjs'],
}

export class TSConfigFile {
static FILENAME = 'tsconfig.json'

Expand Down Expand Up @@ -92,4 +145,74 @@ export class TSConfigFile {
resolvePath (importPath: string): string[] {
return this.pathResolver.resolve(importPath)
}

private extifyLookupPaths (filePaths: string[]): string[] {
return filePaths.flatMap(filePath => {
let extensions = extensionMappings['.js']
let extlessPath = filePath

for (const ext of JSExtensions) {
if (filePath.endsWith(ext)) {
extensions = extensionMappings[ext]
extlessPath = filePath.substring(0, filePath.length - ext.length)
}
}

return extensions.map(ext => path.resolve(this.basePath, extlessPath + ext))
})
}

collectLookupPaths (filePath: string): string[] {
let {
outDir,
rootDir,
rootDirs,
composite,
} = this.jsonFile.data.compilerOptions ?? {}

const candidates = []

if (outDir === undefined) {
candidates.push(filePath)
return this.extifyLookupPaths(candidates) // Nothing more we can do.
}

if (composite === undefined) {
composite = false
}

// Inferred rootDir is tsconfig directory if composite === true.
if (rootDir === undefined && composite) {
rootDir = '.'
}

// If we still don't have a root, we should calculate the longest common
// path among input files, but that's a lot of effort. Assume tsconfig
// directory and hope for the best.
if (rootDir === undefined) {
rootDir = '.'
}

const absoluteOutDir = path.resolve(this.basePath, outDir)
const relativePath = path.relative(absoluteOutDir, filePath)

// If the file is outside outDir, then assume we're looking for
// something that wasn't compiled using this tsconfig (or at all), and
// stop looking.
if (relativePath.startsWith('..')) {
candidates.push(filePath)
return this.extifyLookupPaths(candidates)
}

candidates.push(path.resolve(this.basePath, rootDir, relativePath))

// Assume that our inferred (or user specified) rootDir is enough to cover
// the same conditions we'd have to infer rootDirs, and only add rootDirs
// if they're actually set.
for (const multiRootDir of rootDirs ?? []) {
candidates.push(path.resolve(this.basePath, multiRootDir, relativePath))
}

return this.extifyLookupPaths(candidates)
}
}

0 comments on commit 5ce630d

Please sign in to comment.