import { existsSync, readFileSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; export interface PomInfo { groupId: string; artifactId: string; version: string; name: string; description: string; packaging: string; modules: string[]; profiles: string[]; } export interface ProjectNode { groupId: string; artifactId: string; version: string; name: string; description: string; packaging: string; pomPath: string; relativePath: string; modules?: Record; } export function findProjectRoot(dir: string): string | null { let current = resolve(dir); let lastPomDir: string | null = null; while (true) { if (existsSync(join(current, "pom.xml"))) { lastPomDir = current; } const parent = dirname(current); if (parent === current) break; current = parent; } return lastPomDir; } function extractTag(xml: string, tag: string): string { const match = xml.match(new RegExp(`<${tag}>([^<]+)<\\/${tag}>`)); return match ? match[1].trim() : ""; } function projectCoordinateSection(xml: string): string { const blockStart = xml.search(/<(dependencies|dependencyManagement|build|profiles|modules|reporting|distributionManagement|repositories)[\s>]/); return blockStart === -1 ? xml : xml.slice(0, blockStart); } function extractModules(xml: string): string[] { const modulesBlock = xml.match(/([\s\S]*?)<\/modules>/); if (!modulesBlock) return []; return [...modulesBlock[1].matchAll(/([^<]+)<\/module>/g)].map((m) => m[1].trim()); } function extractProfiles(xml: string): string[] { const profiles: string[] = []; for (const profilesBlock of xml.matchAll(/([\s\S]*?)<\/profiles>/g)) { for (const profile of profilesBlock[1].matchAll(/[\s\S]*?([^<]+)<\/id>[\s\S]*?<\/profile>/g)) { profiles.push(profile[1].trim()); } } return profiles; } export function parsePom(pomPath: string): PomInfo { const raw = readFileSync(pomPath, "utf8"); const parentBlock = raw.match(/([\s\S]*?)<\/parent>/); const parentGroupId = parentBlock ? extractTag(parentBlock[1], "groupId") : ""; const parentVersion = parentBlock ? extractTag(parentBlock[1], "version") : ""; const xml = raw.replace(/[\s\S]*?<\/parent>/g, ""); const coords = projectCoordinateSection(xml); const groupId = extractTag(coords, "groupId") || parentGroupId; const artifactId = extractTag(coords, "artifactId"); const version = extractTag(coords, "version") || parentVersion; const name = extractTag(coords, "name"); const description = extractTag(coords, "description"); const packaging = extractTag(xml, "packaging") || "jar"; const modules = extractModules(xml); const profiles = extractProfiles(raw); return { groupId, artifactId, version, name, description, packaging, modules, profiles }; } export function buildProjectTree(projectRoot: string): ProjectNode { return buildNode(projectRoot, "."); } function buildNode(nodeDir: string, relPath: string): ProjectNode { const pomPath = join(nodeDir, "pom.xml"); const pom = parsePom(pomPath); if (pom.modules.length > 0 && pom.packaging !== "pom") { throw new Error(`${pomPath}: declares but packaging is '${pom.packaging}' (expected 'pom')`); } const modules: Record = Object.fromEntries( pom.modules.map((mod) => { const childDir = join(nodeDir, mod); const childRel = relPath === "." ? mod : `${relPath}/${mod}`; return [mod, buildNode(childDir, childRel)]; }), ); return { groupId: pom.groupId, artifactId: pom.artifactId, version: pom.version, name: pom.name, description: pom.description, packaging: pom.packaging, pomPath, relativePath: relPath, ...(Object.keys(modules).length > 0 ? { modules } : {}), }; } /** * Returns true if the pom.xml at `pomPath` declares a plugin with the given * `artifactId` inside `` or ``. * Strips XML comments before matching to avoid false positives. */ export function pomHasPlugin(pomPath: string, pluginArtifactId: string): boolean { let raw: string; try { raw = readFileSync(pomPath, "utf8"); } catch { return false; } // Strip XML comments to avoid matching commented-out plugins const stripped = raw.replace(//g, ""); const buildBlock = stripped.match(/[\s\S]*?<\/build>/); if (!buildBlock) return false; const pluginIds = [...buildBlock[0].matchAll(/([^<]+)<\/artifactId>/g)].map((m) => m[1].trim()); return pluginIds.includes(pluginArtifactId); } export function stripInternalFields(node: ProjectNode): Omit & { modules?: Record> } { const { relativePath: _, pomPath: __, modules, description, name, ...rest } = node; const strippedModules = modules ? Object.fromEntries(Object.entries(modules).map(([k, v]) => [k, stripInternalFields(v)])) : undefined; return { ...rest, name, description, ...(strippedModules ? { modules: strippedModules } : {}), }; } export function resolveCurrentProject(tree: ProjectNode, projectRoot: string, cwd: string): ProjectNode | null { const rel = relative(projectRoot, cwd); return findByRelativePath(tree, rel === "" ? "." : rel); } function findByRelativePath(node: ProjectNode, targetRel: string): ProjectNode | null { if (node.relativePath === targetRel) return node; for (const child of Object.values(node.modules ?? {})) { const found = findByRelativePath(child, targetRel); if (found) return found; } return null; }