vite-tsconfig-paths
Version:
Vite resolver for TypeScript compilerOptions.paths
688 lines (681 loc) • 21.4 kB
JavaScript
// src/index.ts
import * as fs2 from "fs";
import * as vite2 from "vite";
// src/debug.ts
import createDebug from "debug";
var debug = createDebug("vite-tsconfig-paths");
if (process.env.TEST === "vite-tsconfig-paths") {
createDebug.enable("vite-tsconfig-paths");
}
// src/logFile.ts
import { createWriteStream, statSync, writeFileSync } from "fs";
function createLogFile(logFilePath) {
let mtime;
try {
mtime = statSync(logFilePath).mtime.getTime();
} catch (e) {
}
if (!mtime || Date.now() - mtime > 1e4) {
debug("Clearing log file:", logFilePath);
writeFileSync(logFilePath, "");
}
const logFile = createWriteStream(logFilePath, {
flags: "a",
encoding: "utf-8"
});
return {
write(...event) {
logFile.write(event[0] + ": " + JSON.stringify(event[1]) + "\n");
}
};
}
// src/path.ts
import * as os from "os";
import * as path from "path";
import * as vite from "vite";
var isWindows = os.platform() == "win32";
var normalize = (p) => {
let output = vite.normalizePath(p);
if (isWindows && output[1] === ":") {
output = output[0].toUpperCase() + output.substring(1);
}
return output;
};
var parse2 = path.parse;
var resolve = isWindows ? (...paths) => normalize(path.win32.resolve(...paths)) : path.posix.resolve;
var isAbsolute = isWindows ? path.win32.isAbsolute : path.posix.isAbsolute;
var join = path.posix.join;
var relative = path.posix.relative;
var basename = path.posix.basename;
var dirname2 = path.dirname;
var relativeImportRE = /^\.\.?(\/|$)/;
// src/resolver.ts
import globRex from "globrex";
import * as fs from "fs";
import { readdir } from "fs/promises";
import { isAbsolute as isAbsolute2, join as join2, relative as relative2 } from "path";
import { inspect } from "util";
import * as tsconfck from "tsconfck";
// src/mappings.ts
import { resolve as resolve2 } from "path";
function resolvePathMappings(paths, base) {
const sortedPatterns = Object.keys(paths).sort(
(a, b) => getPrefixLength(b) - getPrefixLength(a)
);
const resolved = [];
for (let pattern of sortedPatterns) {
const relativePaths = paths[pattern];
pattern = escapeStringRegexp(pattern).replace(/\*/g, "(.+)");
resolved.push({
pattern: new RegExp("^" + pattern + "$"),
paths: relativePaths.map((relativePath) => resolve2(base, relativePath))
});
}
return resolved;
}
function getPrefixLength(pattern) {
const prefixLength = pattern.indexOf("*");
return pattern.substr(0, prefixLength).length;
}
function escapeStringRegexp(string) {
return string.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/-/g, "\\x2d");
}
// src/resolver.ts
var notApplicable = [void 0, false];
var notFound = [void 0, true];
var emptyDirectory = {
projects: Object.freeze([]),
lazyDiscovery: false
};
function createTsconfigResolvers({
projectRoot,
workspaceRoot,
skip = () => false,
logFile,
logger,
...opts
}) {
let initializing;
let directoryCache;
let resolversByProject;
let isFirstParseError = true;
let hasTypeScriptDep = false;
if (opts.parseNative) {
try {
const pkgJson = fs.readFileSync(
join2(workspaceRoot, "package.json"),
"utf8"
);
const pkg = JSON.parse(pkgJson);
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
hasTypeScriptDep = "typescript" in deps;
} catch (e) {
if (e.code != "ENOENT") {
throw e;
}
}
}
const configNames = opts.configNames || ["tsconfig.json", "jsconfig.json"];
debug(
"Only tsconfig files with a name in this list are discoverable:",
configNames
);
const parseProject = async (tsconfigFile) => {
tsconfigFile = normalize(tsconfigFile);
try {
return hasTypeScriptDep ? await tsconfck.parseNative(tsconfigFile) : await tsconfck.parse(tsconfigFile);
} catch (error) {
if (opts.ignoreConfigErrors) {
debug("[!] Failed to parse tsconfig file at %s", tsconfigFile);
if (isFirstParseError) {
debug("Remove the `ignoreConfigErrors` option to see the error.");
}
} else {
logger.error(
'[tsconfig-paths] An error occurred while parsing "' + tsconfigFile + '". See below for details.' + (isFirstParseError ? " To disable this message, set the `ignoreConfigErrors` option to true." : ""),
{ error }
);
if (!logger.hasErrorLogged(error)) {
console.error(error);
}
}
isFirstParseError = false;
return null;
}
};
let onBeforeAddProject;
let onParseError;
const addProject = (project, data) => {
const tsconfigFile = project.tsconfigFile;
const dir = normalize(dirname2(tsconfigFile));
data != null ? data : data = directoryCache.get(dir);
if (data == null ? void 0 : data.projects.some((p) => p.tsconfigFile === tsconfigFile)) {
return;
}
onBeforeAddProject == null ? void 0 : onBeforeAddProject(project);
if (project.referenced) {
project.referenced.forEach((projectRef) => {
addProject(projectRef);
});
data = directoryCache.get(dir);
}
const resolver = createResolver(project, opts, logFile);
if (resolver) {
resolversByProject.set(project, resolver);
}
if (!data || data === emptyDirectory) {
directoryCache.set(
dir,
data = {
projects: [],
lazyDiscovery: null
}
);
}
data.projects.push(project);
};
const loadProject = async (tsconfigFile, data) => {
const project = await parseProject(tsconfigFile);
if (project) {
addProject(project, data);
} else {
onParseError == null ? void 0 : onParseError(tsconfigFile);
}
};
const sortProjects = (projects) => {
projects.sort(
(left, right) => left.tsconfigFile.localeCompare(right.tsconfigFile)
);
};
const processConfigFile = async (dir, name, data = directoryCache.get(dir)) => {
if (!data) {
return;
}
const file = join(dir, name);
if (data.projects.some((p) => p.tsconfigFile === file)) {
return;
}
await loadProject(file, data);
};
const loadEagerProjects = async () => {
let projectPaths;
if (opts.projects) {
projectPaths = opts.projects.map((file) => {
if (!file.endsWith(".json")) {
file = join2(file, "tsconfig.json");
}
return resolve(projectRoot, file);
});
} else {
if (opts.projectDiscovery === "lazy") {
return;
}
projectPaths = await tsconfck.findAll(workspaceRoot, {
configNames,
skip
});
}
debug("Eagerly parsing these projects:", projectPaths);
await Promise.all(Array.from(new Set(projectPaths), (p) => loadProject(p)));
for (const data of directoryCache.values()) {
sortProjects(data.projects);
}
};
const resetResolvers = () => {
directoryCache = /* @__PURE__ */ new Map();
resolversByProject = /* @__PURE__ */ new WeakMap();
initializing = loadEagerProjects();
};
const discoverProjects = async (dir, data) => {
debug("Searching directory for tsconfig files:", dir);
const names = await readdir(dir).catch(() => []);
await Promise.all(
names.filter((name) => configNames.includes(name)).map((name) => {
return processConfigFile(dir, name, data);
})
);
if (data.projects.length) {
sortProjects(data.projects);
if (debug.enabled) {
debug(
`Directory "${dir}" contains the following tsconfig files:`,
data.projects.map((p) => basename(p.tsconfigFile))
);
}
} else {
directoryCache.set(dir, emptyDirectory);
debug("No tsconfig files found in directory:", dir);
}
};
const getResolvers = async function* (importer) {
var _a;
await initializing;
let dir = normalize(importer);
const { root } = parse2(dir);
while (dir !== (dir = dirname2(dir)) && dir !== root) {
let data = directoryCache.get(dir);
if (opts.projectDiscovery === "lazy") {
if (!data) {
if (skip(basename(dir))) {
directoryCache.set(dir, emptyDirectory);
continue;
}
directoryCache.set(
dir,
data = {
projects: [],
lazyDiscovery: null
}
);
}
await ((_a = data.lazyDiscovery) != null ? _a : data.lazyDiscovery = discoverProjects(dir, data));
} else if (!data) {
continue;
}
for (const project of data.projects) {
const resolver = resolversByProject.get(project);
if (resolver) {
yield resolver;
}
}
}
};
const watchProjects = (watcher) => {
onBeforeAddProject = (project) => {
var _a;
watcher.add(project.tsconfigFile);
(_a = project.extended) == null ? void 0 : _a.forEach((parent) => {
watcher.add(parent.tsconfigFile);
});
};
onParseError = (tsconfigFile) => {
watcher.add(tsconfigFile);
};
watcher.on("all", (event, file) => {
const normalizedFile = normalize(file);
if (!normalizedFile.endsWith(".json") || !isAbsolute(normalizedFile)) {
return;
}
if (event === "add") {
if (configNames.includes(basename(normalizedFile))) {
processConfigFile(
dirname2(normalizedFile),
basename(normalizedFile)
).catch(console.error);
}
} else if (event === "change" || event === "unlink") {
invalidateConfigFile(
dirname2(normalizedFile),
basename(normalizedFile),
event
);
}
});
function invalidateConfigFile(dir, name, event) {
const data = directoryCache.get(dir);
if (!data) {
return;
}
const file = join(dir, name);
const index = data.projects.findIndex(
(project) => project.tsconfigFile === file
);
if (index !== -1) {
const project = data.projects[index];
debug(
`Unloading project because of ${event} event:`,
project.tsconfigFile
);
resolversByProject.delete(project);
data.projects.splice(index, 1);
if (event === "change") {
if (opts.projectDiscovery === "lazy") {
data.lazyDiscovery = null;
} else {
loadProject(project.tsconfigFile, data).then(() => {
sortProjects(data.projects);
}).catch(console.error);
}
}
}
}
};
return {
reset: resetResolvers,
get: getResolvers,
watch: watchProjects
};
}
function createResolver(project, opts, logFile) {
var _a, _b, _c, _d, _e;
const configPath = project.tsconfigFile;
const config = project.tsconfig;
debug("Config loaded:", inspect({ configPath, config }, false, 10, true));
if (((_a = config.files) == null ? void 0 : _a.length) == 0 && !((_b = config.include) == null ? void 0 : _b.length)) {
debug(
`[!] Skipping "${configPath}" as no files can be matched since "files" is empty and "include" is missing or empty.`
);
return null;
}
const compilerOptions = config.compilerOptions || {};
const { baseUrl, paths } = compilerOptions;
const resolveWithBaseUrl = baseUrl ? async (viteResolve, id, importer) => {
if (id[0] === "/") {
return;
}
const absoluteId = join2(baseUrl, id);
const resolvedId = await viteResolve(absoluteId, importer);
if (resolvedId) {
if (resolvedId.endsWith(".json") && !id.endsWith(".json")) {
return;
}
logFile == null ? void 0 : logFile.write("resolvedWithBaseUrl", {
importer,
id,
resolvedId,
configPath
});
return resolvedId;
}
} : void 0;
let resolveId;
if (paths) {
const pathsRootDir = resolvePathsRootDir(project);
const pathMappings = resolvePathMappings(paths, pathsRootDir);
const resolveWithPaths = async (viteResolve, id, importer) => {
const candidates = logFile ? [] : null;
for (const mapping of pathMappings) {
const match = id.match(mapping.pattern);
if (!match) {
continue;
}
for (let pathTemplate of mapping.paths) {
let starCount = 0;
const mappedId = pathTemplate.replace(/\*/g, () => {
const matchIndex = Math.min(++starCount, match.length - 1);
return match[matchIndex];
});
candidates == null ? void 0 : candidates.push(mappedId);
const resolvedId = await viteResolve(mappedId, importer);
if (resolvedId) {
logFile == null ? void 0 : logFile.write("resolvedWithPaths", {
importer,
id,
resolvedId,
configPath
});
return resolvedId;
}
}
}
logFile == null ? void 0 : logFile.write("notFound", {
importer,
id,
candidates,
configPath
});
};
if (resolveWithBaseUrl) {
resolveId = async (viteResolve, id, importer) => {
var _a2;
return (_a2 = await resolveWithPaths(viteResolve, id, importer)) != null ? _a2 : await resolveWithBaseUrl(viteResolve, id, importer);
};
} else {
resolveId = resolveWithPaths;
}
} else if (resolveWithBaseUrl) {
resolveId = resolveWithBaseUrl;
} else {
debug(`[!] Skipping "${configPath}" as no paths or baseUrl are defined.`);
return null;
}
const configDir = normalize(dirname2(configPath));
let outDir = compilerOptions.outDir && normalize(compilerOptions.outDir);
if (outDir && isAbsolute(outDir)) {
outDir = relative(configDir, outDir);
}
const isIncludedRelative = getIncluder(
(_c = config.include) == null ? void 0 : _c.map((p) => ensureRelative(configDir, p)),
(_d = config.exclude) == null ? void 0 : _d.map((p) => ensureRelative(configDir, p)),
outDir
);
const isImporterSupported = opts.loose ? () => true : (_e = opts.importerFilter) != null ? _e : (() => {
const extensionFilter = compilerOptions.allowJs || basename(configPath).startsWith("jsconfig.") ? /\.(astro|mdx|svelte|vue|[mc]?[jt]sx?)$/ : /\.[mc]?tsx?$/;
return (importer) => extensionFilter.test(importer);
})();
const resolutionCache = /* @__PURE__ */ new Map();
const hashQueryPattern = /[#?].+$/;
const queryPattern = /\?.+$/;
const dtsPattern = /\.d\.ts(\?|$)/;
return async (viteResolve, id, importer) => {
var _a2;
const importerFile = normalize(importer.replace(hashQueryPattern, ""));
if (!isImporterSupported(importerFile)) {
logFile == null ? void 0 : logFile.write("unsupportedExtension", { importer, id });
return notApplicable;
}
const relativeImporterFile = relative(configDir, importerFile);
if (!isIncludedRelative(relativeImporterFile)) {
logFile == null ? void 0 : logFile.write("configMismatch", { importer, id, configPath });
return notApplicable;
}
const query = (_a2 = queryPattern.exec(id)) == null ? void 0 : _a2[0];
if (query) {
id = id.slice(0, -query.length);
}
let resolvedId = resolutionCache.get(id);
if (resolvedId) {
logFile == null ? void 0 : logFile.write("resolvedFromCache", {
importer,
id,
resolvedId,
configPath
});
} else {
resolvedId = await resolveId(viteResolve, id, importer);
if (!resolvedId) {
return notFound;
}
resolutionCache.set(id, resolvedId);
}
if (dtsPattern.test(resolvedId)) {
logFile == null ? void 0 : logFile.write("resolvedToDeclarationFile", {
importer,
id,
resolvedId,
configPath
});
return notApplicable;
}
if (query) {
resolvedId += query;
}
return [resolvedId, true];
};
}
function resolvePathsRootDir(project) {
var _a, _b;
if (project.result) {
const { options } = project.result;
if (options && typeof options.pathsBasePath === "string") {
return options.pathsBasePath;
}
return dirname2(project.tsconfigFile);
}
const baseUrl = (_a = project.tsconfig.compilerOptions) == null ? void 0 : _a.baseUrl;
if (baseUrl) {
return baseUrl;
}
const projectWithPaths = (_b = project.extended) == null ? void 0 : _b.find(
(project2) => {
var _a2;
return (_a2 = project2.tsconfig.compilerOptions) == null ? void 0 : _a2.paths;
}
);
return dirname2((projectWithPaths != null ? projectWithPaths : project).tsconfigFile);
}
var defaultInclude = ["**/*"];
var defaultExclude = [
"**/node_modules",
"**/bower_components",
"**/jspm_packages"
];
function getIncluder(includePaths = defaultInclude, excludePaths = defaultExclude, outDir) {
if (outDir) {
excludePaths = excludePaths.concat(outDir);
}
if (includePaths.length || excludePaths.length) {
const includers = [];
const excluders = [];
includePaths.forEach(addCompiledGlob, includers);
excludePaths.forEach(addCompiledGlob, excluders);
if (debug.enabled) {
debug(`Compiled tsconfig globs:`, {
include: {
globs: includePaths,
regexes: includers
},
exclude: {
globs: excludePaths,
regexes: excluders
}
});
}
return (id) => {
id = id.replace(/\?.+$/, "");
if (!relativeImportRE.test(id)) {
id = "./" + id;
}
const test = (glob) => glob.test(id);
return includers.some(test) && !excluders.some(test);
};
}
return () => true;
}
function addCompiledGlob(glob) {
const endsWithGlob = glob.split("/").pop().includes("*");
const relativeGlob = relativeImportRE.test(glob) ? glob : "./" + glob;
if (endsWithGlob) {
this.push(compileGlob(relativeGlob));
} else {
this.push(compileGlob(relativeGlob + "/**"));
if (/\.\w+$/.test(glob)) {
this.push(compileGlob(relativeGlob));
}
}
}
function compileGlob(glob) {
return globRex(glob, {
extended: true,
globstar: true
}).regex;
}
function ensureRelative(dir, path2) {
return isAbsolute2(path2) ? relative2(dir, path2) : path2;
}
// src/index.ts
var src_default = (opts = {}) => {
let tsconfigResolvers;
let isFirstBuild = true;
const logFile = opts.logFile ? createLogFile(
opts.logFile === true ? "vite-tsconfig-paths.log" : opts.logFile
) : null;
debug("Plugin options:", opts);
const plugin = {
name: "vite-tsconfig-paths",
enforce: "pre",
configResolved(config) {
let projectRoot;
let workspaceRoot;
if (opts.root) {
projectRoot = workspaceRoot = resolve(config.root, opts.root);
} else {
projectRoot = normalize(config.root);
workspaceRoot = normalize(vite2.searchForWorkspaceRoot(config.root));
}
debug("Project root: ", projectRoot);
debug("Workspace root:", workspaceRoot);
tsconfigResolvers = createTsconfigResolvers({
...opts,
projectRoot,
workspaceRoot,
logFile,
logger: config.logger,
skip(dir) {
if (dir === ".git" || dir === "node_modules") {
return true;
}
if (typeof opts.skip === "function") {
return opts.skip(dir);
}
return false;
}
});
tsconfigResolvers.reset();
},
configureServer(server) {
tsconfigResolvers.watch(server.watcher);
},
buildStart() {
if (isFirstBuild) {
isFirstBuild = false;
return;
}
tsconfigResolvers.reset();
},
async resolveId(id, importer, options) {
if (!importer) {
logFile == null ? void 0 : logFile.write("emptyImporter", { importer, id });
return;
}
if (relativeImportRE.test(id)) {
logFile == null ? void 0 : logFile.write("relativeId", { importer, id });
return;
}
if (id.includes("\0")) {
logFile == null ? void 0 : logFile.write("virtualId", { importer, id });
return;
}
let importerFile = importer;
if (importer[0] === "\0") {
const index = importer.indexOf("?");
if (index !== -1) {
const query = normalize(importer.slice(index + 1));
if (isAbsolute(query) && fs2.existsSync(query)) {
debug("Rewriting virtual importer to real file:", importer);
importerFile = query;
} else {
logFile == null ? void 0 : logFile.write("virtualImporter", { importer, id });
return;
}
} else {
logFile == null ? void 0 : logFile.write("virtualImporter", { importer, id });
return;
}
}
const resolveOptions = { ...options, skipSelf: true };
const viteResolve = async (id2, importer2) => {
var _a;
return (_a = await this.resolve(id2, importer2, resolveOptions)) == null ? void 0 : _a.id;
};
for await (const resolveId of tsconfigResolvers.get(importerFile)) {
const [resolved, matched] = await resolveId(
viteResolve,
id,
importerFile
);
if (resolved) {
return resolved;
}
if (matched) {
break;
}
}
}
};
return plugin;
};
export {
src_default as default
};
//# sourceMappingURL=index.js.map