import fs from 'node:fs/promises'
import path from 'node:path'
const DEPENDENCY_PATTERNS = [
/import[\s\S]*?['"](.{3,}?)['"]/gi,
/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi,
/export[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi,
/require\(['"`](.+)['"`]\)/gi,
]
const JS_EXTENSIONS = ['.js', '.cjs', '.mjs']
const JS_RESOLUTION_ORDER = ['', '.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx']
const TS_RESOLUTION_ORDER = ['', '.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx']
async function resolveWithExtension(file: string, extensions: string[]) {
for (let ext of extensions) {
let full = `${file}${ext}`
let stats = await fs.stat(full).catch(() => null)
if (stats?.isFile()) return full
}
for (let ext of extensions) {
let full = `${file}/index${ext}`
let exists = await fs.access(full).then(
() => true,
() => false,
)
if (exists) {
return full
}
}
return null
}
async function traceDependencies(
seen: Set<string>,
filename: string,
base: string,
ext: string,
): Promise<void> {
let extensions = JS_EXTENSIONS.includes(ext) ? JS_RESOLUTION_ORDER : TS_RESOLUTION_ORDER
let absoluteFile = await resolveWithExtension(path.resolve(base, filename), extensions)
if (absoluteFile === null) return
if (seen.has(absoluteFile)) return
seen.add(absoluteFile)
base = path.dirname(absoluteFile)
ext = path.extname(absoluteFile)
let contents = await fs.readFile(absoluteFile, 'utf-8')
let promises = []
for (let pattern of DEPENDENCY_PATTERNS) {
for (let match of contents.matchAll(pattern)) {
if (!match[1].startsWith('.')) continue
promises.push(traceDependencies(seen, match[1], base, ext))
}
}
await Promise.all(promises)
}
export async function getModuleDependencies(absoluteFilePath: string) {
let seen = new Set<string>()
await traceDependencies(
seen,
absoluteFilePath,
path.dirname(absoluteFilePath),
path.extname(absoluteFilePath),
)
return Array.from(seen)
}