@@ -7,7 +7,8 @@ import { XMLParser } from 'fast-xml-parser'
77
88import { getLicense } from '../license/license_utils.js'
99import 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
1213import 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+ }
0 commit comments