Skip to content

Commit df57b31

Browse files
Strum355claude
andcommitted
refactor: move Maven discovery logic from workspace.js to java_maven.js
Address review feedback: provider-specific discovery functions don't belong in workspace.js. Extract traverseForWrapper to tools.js as shared utility, update base_java.js to delegate to it, and move all Maven workspace discovery functions (discoverMavenModules, resolveMavenBinary, collectMavenModules, listMavenModules, parseMavenModuleList) to their provider file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fcd428a commit df57b31

6 files changed

Lines changed: 157 additions & 200 deletions

File tree

src/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import analysis from './analysis.js'
77
import fs from 'node:fs'
88
import { getCustom } from "./tools.js";
99
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
10+
import { discoverMavenModules } from './providers/java_maven.js'
1011
import {
11-
discoverMavenModules,
1212
discoverWorkspaceCrates,
1313
discoverWorkspacePackages,
1414
filterManifestPathsByDiscoveryIgnore,
@@ -100,7 +100,7 @@ export {
100100
* @param {any} valueToBePrinted - The value to log.
101101
* @private
102102
*/
103-
function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
103+
function logOptionsAndEnvironmentsVariables(alongsideText, valueToBePrinted) {
104104
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
105105
console.log(`${alongsideText}: ${valueToBePrinted} ${EOL}`)
106106
}
@@ -112,9 +112,9 @@ function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
112112
*/
113113
function readAndPrintVersionFromPackageJson() {
114114
let dirName
115-
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
115+
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
116116
dirName = import.meta.dirname
117-
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
117+
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
118118
if (!dirName) {
119119
dirName = url.fileURLToPath(new URL('.', import.meta.url));
120120
}

src/providers/base_java.js

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import fs from 'node:fs'
21
import path from 'node:path'
32

43
import { PackageURL } from 'packageurl-js'
54

6-
import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"
5+
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from "../tools.js"
76

87
/** @typedef {import('../provider').Provider} */
98

@@ -145,7 +144,8 @@ export default class Base_Java {
145144

146145
const useWrapper = getWrapperPreference(this.globalBinary, opts)
147146
if (useWrapper) {
148-
const wrapper = this.traverseForWrapper(manifestPath)
147+
const manifestDir = path.dirname(this.normalizePath(manifestPath))
148+
const wrapper = traverseForWrapper(manifestDir)
149149
if (wrapper !== undefined) {
150150
try {
151151
this._invokeCommand(wrapper, ['--version'], {cwd: manifestDir})
@@ -168,39 +168,6 @@ export default class Base_Java {
168168
return toolPath
169169
}
170170

171-
/**
172-
*
173-
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
174-
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
175-
* to the root of the drive the manifest is on (assumes absolute path is given)
176-
* @returns {string|undefined}
177-
*/
178-
traverseForWrapper(startingManifest, repoRoot = undefined) {
179-
const normalizedManifest = this.normalizePath(startingManifest);
180-
const currentDir = this.normalizePath(path.dirname(normalizedManifest));
181-
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
182-
const wrapperPath = path.join(currentDir, this.localWrapper);
183-
184-
try {
185-
fs.accessSync(wrapperPath, fs.constants.X_OK);
186-
return wrapperPath;
187-
}
188-
catch (error) {
189-
if (error.code === 'ENOENT') {
190-
const rootDir = path.parse(currentDir).root;
191-
if (currentDir === repoRoot || currentDir === rootDir) {
192-
return undefined;
193-
}
194-
const parentDir = path.dirname(currentDir);
195-
if (parentDir === currentDir || parentDir === rootDir) {
196-
return undefined;
197-
}
198-
return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
199-
}
200-
throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
201-
}
202-
}
203-
204171
normalizePath(thePath) {
205172
const normalized = path.resolve(thePath).normalize();
206173
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;

src/providers/java_maven.js

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { XMLParser } from 'fast-xml-parser'
77

88
import { getLicense } from '../license/license_utils.js'
99
import Sbom from '../sbom.js'
10-
import { getCustom } from '../tools.js'
10+
import { getCustom, getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from '../tools.js'
11+
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
1112

1213
import Base_java, { ecosystem_maven } from "./base_java.js";
1314

@@ -309,3 +310,114 @@ export default class Java_maven extends Base_java {
309310
return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0
310311
}
311312
}
313+
314+
const DEFAULT_MAVEN_DISCOVERY_IGNORE = [
315+
'**/target/**',
316+
]
317+
318+
/**
319+
* Resolve the Maven binary, respecting wrapper preference.
320+
*
321+
* @param {string} startDir - Directory from which to start the wrapper search
322+
* @param {object} [opts={}]
323+
* @returns {string} Path to the Maven binary
324+
*/
325+
function resolveMavenBinary(startDir, opts = {}) {
326+
const localWrapper = 'mvnw' + (process.platform === 'win32' ? '.cmd' : '')
327+
const useWrapper = getWrapperPreference('mvn', opts)
328+
if (useWrapper) {
329+
const wrapper = traverseForWrapper(startDir, localWrapper)
330+
if (wrapper !== undefined) {
331+
return wrapper
332+
}
333+
}
334+
return getCustomPath('mvn', opts)
335+
}
336+
337+
/**
338+
* Discover all pom.xml manifest paths in a Maven multi-module project.
339+
*
340+
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pom.xml)
341+
* @param {object} [opts={}]
342+
* @returns {Promise<string[]>} Paths to pom.xml files (absolute)
343+
*/
344+
export async function discoverMavenModules(workspaceRoot, opts = {}) {
345+
const root = path.resolve(workspaceRoot)
346+
const rootPom = path.join(root, 'pom.xml')
347+
348+
if (!fs.existsSync(rootPom)) {
349+
return []
350+
}
351+
352+
const mvnBin = resolveMavenBinary(root, opts)
353+
const visited = new Set()
354+
const manifestPaths = [rootPom]
355+
356+
collectMavenModules(root, mvnBin, visited, manifestPaths)
357+
358+
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_MAVEN_DISCOVERY_IGNORE]
359+
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
360+
}
361+
362+
/**
363+
* @param {string} dir - Absolute path to directory containing pom.xml
364+
* @param {string} mvnBin - Maven binary path
365+
* @param {Set<string>} visited - Already-visited directories (cycle guard)
366+
* @param {string[]} manifestPaths - Accumulator for discovered pom.xml paths
367+
*/
368+
function collectMavenModules(dir, mvnBin, visited, manifestPaths) {
369+
const resolvedDir = path.resolve(dir)
370+
if (visited.has(resolvedDir)) {
371+
return
372+
}
373+
visited.add(resolvedDir)
374+
375+
const modules = listMavenModules(resolvedDir, mvnBin)
376+
for (const mod of modules) {
377+
const moduleDir = path.resolve(resolvedDir, mod)
378+
const modulePom = path.join(moduleDir, 'pom.xml')
379+
if (fs.existsSync(modulePom)) {
380+
manifestPaths.push(modulePom)
381+
collectMavenModules(moduleDir, mvnBin, visited, manifestPaths)
382+
}
383+
}
384+
}
385+
386+
/**
387+
* @param {string} dir - Directory containing pom.xml
388+
* @param {string} mvnBin - Maven binary path
389+
* @returns {string[]} Module directory names (relative to `dir`)
390+
*/
391+
function listMavenModules(dir, mvnBin) {
392+
let output
393+
try {
394+
output = invokeCommand(mvnBin, [
395+
'help:evaluate',
396+
'-Dexpression=project.modules',
397+
'-q',
398+
'-DforceStdout',
399+
'-f', path.join(dir, 'pom.xml'),
400+
'--batch-mode',
401+
], { cwd: dir })
402+
} catch {
403+
return []
404+
}
405+
406+
const raw = output.toString().trim()
407+
if (!raw || raw === 'null') {
408+
return []
409+
}
410+
return parseMavenModuleList(raw)
411+
}
412+
413+
/**
414+
* @param {string} raw - Raw stdout from mvn (e.g. `[module-a, module-b]`)
415+
* @returns {string[]}
416+
*/
417+
function parseMavenModuleList(raw) {
418+
const match = raw.match(/^\[(.+)]$/)
419+
if (!match) {
420+
return []
421+
}
422+
return match[1].split(',').map(s => s.trim()).filter(Boolean)
423+
}

src/tools.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { execFileSync } from "child_process";
2+
import fs from 'node:fs'
3+
import path from 'node:path'
24
import { EOL } from "os";
35

46
import { HttpsProxyAgent } from "https-proxy-agent";
@@ -137,6 +139,34 @@ export function getGitRootDir(cwd) {
137139
}
138140
}
139141

142+
/**
143+
* Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
144+
*
145+
* @param {string} startDir - Absolute directory to start from
146+
* @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
147+
* @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
148+
* @returns {string | undefined}
149+
*/
150+
export function traverseForWrapper(startDir, wrapperName, repoRoot = undefined) {
151+
const currentDir = path.resolve(startDir)
152+
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(currentDir).root
153+
const wrapperPath = path.join(currentDir, wrapperName)
154+
try {
155+
fs.accessSync(wrapperPath, fs.constants.X_OK)
156+
return wrapperPath
157+
} catch {
158+
const rootDir = path.parse(currentDir).root
159+
if (currentDir === repoRoot || currentDir === rootDir) {
160+
return undefined
161+
}
162+
const parentDir = path.dirname(currentDir)
163+
if (parentDir === currentDir || parentDir === rootDir) {
164+
return undefined
165+
}
166+
return traverseForWrapper(parentDir, wrapperName, repoRoot)
167+
}
168+
}
169+
140170
/** this method invokes command string in a process in a synchronous way.
141171
* @param {string} bin - the command to be invoked
142172
* @param {Array<string>} args - the args to pass to the binary

0 commit comments

Comments
 (0)