pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
399 lines (396 loc) • 11.4 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
hasCargoWorkspaceTable,
readCargoDependencyNames,
readCargoPackageName,
readCargoWorkspaceMembers
} from "./chunk-PAK6IDME.js";
import {
getProjectIgnoreMatcher,
isExcludedDirName
} from "./chunk-VN2AXLEX.js";
import {
getWorkspaceManifestMarkers
} from "./chunk-VNMT7C64.js";
import {
normalizeMapKey
} from "./chunk-Q5U6FMDI.js";
// dist/clients/review-graph/workspace-modules.js
import * as fs from "node:fs";
import * as path from "node:path";
var MAX_WORKSPACE_CANDIDATES = 5e3;
var SOURCE_EXTS = /* @__PURE__ */ new Set([
".ts",
".tsx",
".js",
".jsx",
".py",
".go",
".rs",
".rb",
".cpp",
".c",
".cc",
".h",
".hpp"
]);
function readJsonSafe(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch {
return void 0;
}
}
function detectWorkspaceType(cwd) {
const markers = getWorkspaceManifestMarkers(cwd);
if (markers.hasPnpmWorkspaceYaml)
return "pnpm";
if (markers.hasGoWork)
return "go";
if (markers.hasCargoToml) {
try {
const content = fs.readFileSync(path.join(cwd, "Cargo.toml"), "utf-8");
if (hasCargoWorkspaceTable(content)) {
return "cargo";
}
} catch {
}
}
if (markers.hasPackageJson) {
const pkgJson = readJsonSafe(path.join(cwd, "package.json"));
const workspaces = normalizeWorkspacePatterns(pkgJson?.workspaces);
if (workspaces.length > 0)
return "npm";
}
return null;
}
function stripQuotes(value) {
return value.trim().replace(/^['"]|['"]$/g, "");
}
function normalizeWorkspacePatterns(value) {
if (Array.isArray(value))
return value.map(stripQuotes).filter(Boolean);
if (value?.packages && Array.isArray(value.packages)) {
return value.packages.map(stripQuotes).filter(Boolean);
}
return [];
}
function expandWorkspacePattern(cwd, pattern) {
const normalized = stripQuotes(pattern.trim()).replace(/\\/g, "/");
if (!normalized || normalized.startsWith("!"))
return [];
const starIndex = normalized.indexOf("*");
if (starIndex === -1) {
const root = path.resolve(cwd, normalized);
return fs.existsSync(path.join(root, "package.json")) || fs.existsSync(path.join(root, "Cargo.toml")) || fs.existsSync(path.join(root, "go.mod")) ? [root] : [];
}
const prefix = normalized.slice(0, starIndex).replace(/\/$/, "");
const baseDir = path.resolve(cwd, prefix || ".");
let entries = [];
try {
entries = fs.readdirSync(baseDir, { withFileTypes: true });
} catch {
return [];
}
const ignoreMatcher = getProjectIgnoreMatcher(cwd);
return entries.filter((entry) => {
const fullPath = path.join(baseDir, entry.name);
return entry.isDirectory() && !isExcludedDirName(entry.name) && !ignoreMatcher.isIgnored(fullPath, true);
}).slice(0, MAX_WORKSPACE_CANDIDATES).map((entry) => path.join(baseDir, entry.name)).filter((root) => fs.existsSync(path.join(root, "package.json")) || fs.existsSync(path.join(root, "Cargo.toml")) || fs.existsSync(path.join(root, "go.mod")));
}
function depsFromPackageJson(pkgJson) {
return Object.keys({
...pkgJson.dependencies,
...pkgJson.devDependencies,
...pkgJson.peerDependencies
});
}
function extractYamlList(content, key) {
const values = [];
let inList = false;
for (const rawLine of content.split(/\r?\n/)) {
const trimmed = rawLine.trim();
if (!trimmed || trimmed.startsWith("#"))
continue;
if (!inList) {
if (trimmed === `${key}:`)
inList = true;
continue;
}
if (trimmed.startsWith("-")) {
const value = stripQuotes(trimmed.slice(1).trim()).replace(/\/$/, "");
if (value)
values.push(value);
continue;
}
if (!rawLine.startsWith(" ") && !rawLine.startsWith(" "))
break;
}
return values;
}
function moduleFromPackageJson(cwd, pkgRoot) {
const pkgJson = readJsonSafe(path.join(pkgRoot, "package.json"));
if (!pkgJson?.name)
return null;
return {
name: pkgJson.name,
root: normalizeMapKey(pkgRoot),
relativePath: path.relative(cwd, pkgRoot).replace(/\\/g, "/"),
entrypoints: pkgJson.main ? [pkgJson.main] : ["index.js"],
internalDeps: [],
externalDeps: depsFromPackageJson(pkgJson)
};
}
function scanPnpmModules(cwd) {
let patterns = [];
try {
const content = fs.readFileSync(path.join(cwd, "pnpm-workspace.yaml"), "utf-8");
patterns = extractYamlList(content, "packages");
} catch {
return [];
}
return patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)).map((pkgRoot) => moduleFromPackageJson(cwd, pkgRoot)).filter((mod) => mod !== null);
}
function scanNpmModules(cwd) {
const pkgJson = readJsonSafe(path.join(cwd, "package.json"));
const patterns = normalizeWorkspacePatterns(pkgJson?.workspaces);
return patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)).map((pkgRoot) => moduleFromPackageJson(cwd, pkgRoot)).filter((mod) => mod !== null);
}
function scanCargoModules(cwd) {
let members = [];
try {
const content = fs.readFileSync(path.join(cwd, "Cargo.toml"), "utf-8");
members = readCargoWorkspaceMembers(content);
} catch {
return [];
}
const modules = [];
for (const member of members) {
const pkgRoot = path.resolve(cwd, member);
const memberToml = path.join(pkgRoot, "Cargo.toml");
let name = "";
let deps = [];
try {
const content = fs.readFileSync(memberToml, "utf-8");
name = readCargoPackageName(content) ?? "";
deps = readCargoDependencyNames(content);
} catch {
continue;
}
if (!name)
continue;
modules.push({
name,
root: normalizeMapKey(pkgRoot),
relativePath: path.relative(cwd, pkgRoot).replace(/\\/g, "/"),
entrypoints: ["src/lib.rs", "src/main.rs"],
internalDeps: [],
externalDeps: deps
});
}
return modules;
}
function scanGoModules(cwd) {
let dirs = [];
try {
const content = fs.readFileSync(path.join(cwd, "go.work"), "utf-8");
dirs = [...content.matchAll(/(?:^|\s)(\.\/?[^\s)]+)/gm)].map((m) => m[1]);
} catch {
return [];
}
const modules = [];
for (const dir of dirs) {
const pkgRoot = path.resolve(cwd, dir);
let name = "";
try {
const content = fs.readFileSync(path.join(pkgRoot, "go.mod"), "utf-8");
const match = content.match(/^module\s+(\S+)/m);
if (match)
name = match[1];
} catch {
continue;
}
if (!name)
continue;
modules.push({
name,
root: normalizeMapKey(pkgRoot),
relativePath: path.relative(cwd, pkgRoot).replace(/\\/g, "/"),
entrypoints: ["."],
internalDeps: [],
externalDeps: []
});
}
return modules;
}
function resolveInternalDeps(modules) {
const nameSet = new Set(modules.map((m) => m.name));
for (const mod of modules) {
const internal = [];
const external = [];
for (const dep of mod.externalDeps) {
if (nameSet.has(dep))
internal.push(dep);
else
external.push(dep);
}
mod.internalDeps = internal;
mod.externalDeps = external;
}
}
function buildDependents(modules) {
const dependents = /* @__PURE__ */ new Map();
for (const mod of modules) {
for (const dep of mod.internalDeps) {
const list = dependents.get(dep) ?? [];
list.push(mod.name);
dependents.set(dep, list);
}
}
return dependents;
}
var _moduleGraphCache = null;
function buildModuleGraph(cwd) {
if (_moduleGraphCache && _moduleGraphCache.cwd === cwd) {
return _moduleGraphCache.graph;
}
const type = detectWorkspaceType(cwd);
if (!type)
return null;
let modules = [];
switch (type) {
case "pnpm":
modules = scanPnpmModules(cwd);
break;
case "npm":
modules = scanNpmModules(cwd);
break;
case "cargo":
modules = scanCargoModules(cwd);
break;
case "go":
modules = scanGoModules(cwd);
break;
}
if (modules.length === 0)
return null;
resolveInternalDeps(modules);
const graph = {
root: cwd,
modules: new Map(modules.map((m) => [m.name, m])),
dependents: buildDependents(modules)
};
_moduleGraphCache = { cwd, graph };
return graph;
}
function clearModuleGraphCache() {
_moduleGraphCache = null;
_moduleSourceFilesMemo.clear();
}
function findModuleForPath(graph, filePath) {
const normalized = normalizeMapKey(filePath);
let best;
for (const mod of graph.modules.values()) {
if (normalized === mod.root || normalized.startsWith(`${mod.root}/`)) {
if (!best || mod.root.length > best.root.length)
best = mod;
}
}
return best;
}
function getDownstreamModules(graph, moduleName) {
const downstream = /* @__PURE__ */ new Set();
const queue = [moduleName];
while (queue.length > 0) {
const current = queue.shift();
for (const dep of graph.dependents.get(current) ?? []) {
if (!downstream.has(dep)) {
downstream.add(dep);
queue.push(dep);
}
}
}
return [...downstream];
}
var _moduleSourceFilesMemo = /* @__PURE__ */ new Map();
var MTIME_GRANULARITY_GUARD_MS = 2e3;
function isMemoEntryFresh(entry) {
const validatedAt = Date.now();
for (const { dir, mtimeMs, stampedAt } of entry.dirs) {
if (validatedAt - stampedAt < MTIME_GRANULARITY_GUARD_MS)
return false;
try {
if (fs.statSync(dir).mtimeMs !== mtimeMs)
return false;
} catch {
return false;
}
}
return true;
}
function getModuleSourceFiles(moduleRoot, maxFiles = 20) {
const root = normalizeMapKey(moduleRoot);
const ignoreMatcher = getProjectIgnoreMatcher(root);
const cached = _moduleSourceFilesMemo.get(root);
if (cached && cached.maxFiles === maxFiles && cached.matcher === ignoreMatcher && isMemoEntryFresh(cached)) {
return [...cached.files];
}
const files = [];
const dirs = [];
let allDirsStamped = true;
const visit = (dir, depth) => {
if (files.length >= maxFiles || depth > 4)
return;
let entries = [];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
try {
dirs.push({
dir,
mtimeMs: fs.statSync(dir).mtimeMs,
stampedAt: Date.now()
});
} catch {
allDirsStamped = false;
}
entries.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of entries) {
if (files.length >= maxFiles)
break;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!isExcludedDirName(entry.name) && !ignoreMatcher.isIgnored(fullPath, true)) {
visit(fullPath, depth + 1);
}
continue;
}
if (!entry.isFile())
continue;
if (!SOURCE_EXTS.has(path.extname(entry.name).toLowerCase()))
continue;
if (ignoreMatcher.isIgnored(fullPath, false))
continue;
files.push(normalizeMapKey(fullPath));
}
};
visit(root, 0);
if (allDirsStamped) {
_moduleSourceFilesMemo.set(root, {
files: [...files],
maxFiles,
dirs,
matcher: ignoreMatcher
});
}
return files;
}
export {
buildModuleGraph,
clearModuleGraphCache,
findModuleForPath,
getDownstreamModules,
getModuleSourceFiles
};