Path Extension Resolver (get-abs-path-with-ext.js)
Examine explicit file extension normalizations, directory index redirects, and tsconfig custom path alias resolutions.
Key File Location: ./dinou/core/get-abs-path-with-ext.js💡 Overview
Node.js's ES module (ESM) resolver is strict: imports like import "./Component" or import "@/utils" fail because they lack file extensions or refer to directories rather than files.
The get-abs-path-with-ext.js utility resolves this restriction. It implements custom ESM path mapping to resolve alias configurations, append appropriate extensions, and locate directory indexes automatically.
📊 Extension Resolution Flow
The flowchart below traces the path resolution cascade for imports:
⚡ Node ESM Resolver Challenges
The resolver handles two main resolution scenarios:
- Alias Resolution: Translates alias config keys (e.g.
@/) to their target physical folder bases (e.g.src/) by parsingtsconfig.json. - Extension Matching: Appends extensions (
.js,.ts,.jsx,.tsx) to find the correct file on disk, or looks for anindexfile if the path is a directory.
⚙️ Complete Code Walkthrough
Below is the full, complete code of get-abs-path-with-ext.js:
const fs = require("fs");
const path = require("path");
const { fileURLToPath, pathToFileURL } = require("url");
// 1. Read tsconfig/jsconfig to build map of alias configurations
function loadTsconfigAliases() {
const cwd = process.cwd();
const tsconfigPath = path.resolve(cwd, "tsconfig.json");
const jsconfigPath = path.resolve(cwd, "jsconfig.json");
const configFile = fs.existsSync(tsconfigPath)
? tsconfigPath
: fs.existsSync(jsconfigPath)
? jsconfigPath
: null;
if (!configFile) return new Map();
let config;
try {
config = JSON.parse(fs.readFileSync(configFile, "utf8"));
} catch (err) {
return new Map();
}
const paths = (config.compilerOptions && config.compilerOptions.paths) || {};
const baseUrl = (config.compilerOptions && config.compilerOptions.baseUrl) || ".";
const absoluteBase = path.resolve(cwd, baseUrl);
const map = new Map();
for (const key of Object.keys(paths)) {
const targets = paths[key];
if (!targets || !targets.length) continue;
let target = Array.isArray(targets) ? targets[0] : targets;
const keyIsWildcard = key.endsWith("/*");
const targetIsWildcard = target.endsWith("/*");
const alias = keyIsWildcard ? key.slice(0, -1) : key;
const targetBase = targetIsWildcard ? target.slice(0, -1) : target;
const resolvedTargetBase = path.resolve(absoluteBase, targetBase);
map.set(alias, { resolvedTargetBase, keyIsWildcard, targetIsWildcard });
}
return map;
}
const aliasMap = loadTsconfigAliases();
// 2. Loop through extensions if the path does not exist
function tryExtensions(filePath) {
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) return filePath;
const exts = [".js", ".ts", ".jsx", ".tsx"];
for (const ext of exts) {
const f = filePath + ext;
if (fs.existsSync(f) && fs.statSync(f).isFile()) return f;
}
// 3. Fallback: If it is a directory, check for index.* files
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
for (const ext of exts) {
const f = path.join(filePath, "index" + ext);
if (fs.existsSync(f) && fs.statSync(f).isFile()) return f;
}
}
return null;
}
exports.getAbsPathWithExt = function getAbsPathWithExt(specifier, context) {
// 4. Resolve via tsconfig aliases
if (aliasMap.size > 0) {
for (const [alias, info] of aliasMap.entries()) {
if (specifier.startsWith(alias)) {
const absPath = path.resolve(info.resolvedTargetBase, specifier.slice(alias.length));
return tryExtensions(absPath);
}
}
}
// 5. Resolve relative pathing relative to parent module
if (specifier.startsWith("./") || specifier.startsWith("../")) {
const parentURL = context.parentURL || pathToFileURL(process.cwd()).href;
const parentDir = path.dirname(fileURLToPath(parentURL));
const absPath = path.resolve(parentDir, specifier);
return tryExtensions(absPath);
}
return null;
};