@@ -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 , invokeCommand } from '../tools.js'
11+ import { filterManifestPathsByDiscoveryIgnore , resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
1112
1213import Base_java , { ecosystem_maven } from "./base_java.js" ;
1314
@@ -309,3 +310,101 @@ 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+ * Discover all pom.xml manifest paths in a Maven multi-module project.
320+ *
321+ * @param {string } workspaceRoot - Absolute or relative path to workspace root (must contain pom.xml)
322+ * @param {object } [opts={}]
323+ * @returns {Promise<string[]> } Paths to pom.xml files (absolute)
324+ */
325+ export async function discoverMavenModules ( workspaceRoot , opts = { } ) {
326+ const root = path . resolve ( workspaceRoot )
327+ const rootPom = path . join ( root , 'pom.xml' )
328+
329+ if ( ! fs . existsSync ( rootPom ) ) {
330+ return [ ]
331+ }
332+
333+ let mvnBin
334+ try {
335+ mvnBin = new Java_maven ( ) . selectToolBinary ( rootPom , opts )
336+ } catch {
337+ return [ rootPom ]
338+ }
339+ const visited = new Set ( )
340+ const manifestPaths = [ rootPom ]
341+
342+ collectMavenModules ( root , mvnBin , visited , manifestPaths )
343+
344+ const ignorePatterns = [ ...resolveWorkspaceDiscoveryIgnore ( opts ) , ...DEFAULT_MAVEN_DISCOVERY_IGNORE ]
345+ return filterManifestPathsByDiscoveryIgnore ( manifestPaths , root , ignorePatterns )
346+ }
347+
348+ /**
349+ * @param {string } dir - Absolute path to directory containing pom.xml
350+ * @param {string } mvnBin - Maven binary path
351+ * @param {Set<string> } visited - Already-visited directories (cycle guard)
352+ * @param {string[] } manifestPaths - Accumulator for discovered pom.xml paths
353+ */
354+ function collectMavenModules ( dir , mvnBin , visited , manifestPaths ) {
355+ const resolvedDir = path . resolve ( dir )
356+ if ( visited . has ( resolvedDir ) ) {
357+ return
358+ }
359+ visited . add ( resolvedDir )
360+
361+ const modules = listMavenModules ( resolvedDir , mvnBin )
362+ for ( const mod of modules ) {
363+ const moduleDir = path . resolve ( resolvedDir , mod )
364+ const modulePom = path . join ( moduleDir , 'pom.xml' )
365+ if ( fs . existsSync ( modulePom ) ) {
366+ manifestPaths . push ( modulePom )
367+ collectMavenModules ( moduleDir , mvnBin , visited , manifestPaths )
368+ }
369+ }
370+ }
371+
372+ /**
373+ * @param {string } dir - Directory containing pom.xml
374+ * @param {string } mvnBin - Maven binary path
375+ * @returns {string[] } Module directory names (relative to `dir`)
376+ */
377+ function listMavenModules ( dir , mvnBin ) {
378+ let output
379+ try {
380+ output = invokeCommand ( mvnBin , [
381+ 'help:evaluate' ,
382+ '-Dexpression=project.modules' ,
383+ '-q' ,
384+ '-DforceStdout' ,
385+ '-f' , path . join ( dir , 'pom.xml' ) ,
386+ '--batch-mode' ,
387+ ] , { cwd : dir } )
388+ } catch {
389+ return [ ]
390+ }
391+
392+ const raw = output . toString ( ) . trim ( )
393+ if ( ! raw || raw . startsWith ( '<modules' ) ) {
394+ return [ ]
395+ }
396+ return parseMavenModuleList ( raw )
397+ }
398+
399+ /**
400+ * @param {string } raw - Raw stdout from mvn help:evaluate -DforceStdout
401+ * @returns {string[] }
402+ */
403+ function parseMavenModuleList ( raw ) {
404+ const parser = new XMLParser ( )
405+ const parsed = parser . parse ( raw )
406+ const entries = parsed ?. strings ?. string
407+ if ( ! entries ) { return [ ] }
408+ const list = Array . isArray ( entries ) ? entries : [ entries ]
409+ return list . map ( s => String ( s ) . trim ( ) ) . filter ( Boolean )
410+ }
0 commit comments