Skip to content

Commit 297ed65

Browse files
Strum355claude
andcommitted
refactor: move Gradle discovery logic from workspace.js to java_gradle.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 Gradle workspace discovery functions (discoverGradleSubprojects, resolveGradleBinary, parseGradleInitScriptOutput, GRADLE_INIT_SCRIPT) to their provider file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 28ca7d0 commit 297ed65

4 files changed

Lines changed: 140 additions & 165 deletions

File tree

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import fs from 'node:fs'
88
import { getCustom } from "./tools.js";
99
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
1010
import { discoverMavenModules } from './providers/java_maven.js'
11+
import { discoverGradleSubprojects } from './providers/java_gradle.js'
1112
import {
12-
discoverGradleSubprojects,
1313
discoverWorkspaceCrates,
1414
discoverWorkspacePackages,
1515
filterManifestPathsByDiscoveryIgnore,

src/providers/java_gradle.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import crypto from 'node:crypto'
12
import fs from 'node:fs'
3+
import os from 'node:os'
24
import path from 'node:path'
35
import { EOL } from 'os'
46

57
import TOML from 'fast-toml'
68

79
import { readLicenseFile } from '../license/license_utils.js'
810
import Sbom from '../sbom.js'
11+
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from '../tools.js'
12+
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
913

1014
import Base_java, { ecosystem_gradle } from "./base_java.js";
1115

@@ -466,3 +470,130 @@ export default class Java_gradle extends Base_java {
466470
return undefined
467471
}
468472
}
473+
474+
const DEFAULT_GRADLE_DISCOVERY_IGNORE = [
475+
'**/build/**',
476+
'**/.gradle/**',
477+
]
478+
479+
/**
480+
* Resolve the Gradle binary, respecting wrapper preference.
481+
*
482+
* @param {string} startDir - Directory from which to start the wrapper search
483+
* @param {import('../index.js').Options} [opts={}]
484+
* @returns {string} Path to the Gradle binary
485+
*/
486+
function resolveGradleBinary(startDir, opts = {}) {
487+
const localWrapper = 'gradlew' + (process.platform === 'win32' ? '.bat' : '')
488+
const useWrapper = getWrapperPreference('gradle', opts)
489+
if (useWrapper) {
490+
const wrapper = traverseForWrapper(startDir, localWrapper)
491+
if (wrapper !== undefined) {
492+
return wrapper
493+
}
494+
}
495+
return getCustomPath('gradle', opts)
496+
}
497+
498+
/** Gradle init script that emits structured project listing. */
499+
const GRADLE_INIT_SCRIPT = `allprojects {
500+
task daListProjects {
501+
doLast {
502+
println "::DA_PROJECT::\${project.path}::\${project.projectDir}"
503+
}
504+
}
505+
}
506+
`
507+
508+
/**
509+
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build.
510+
* Uses a custom init script to get structured project listing.
511+
*
512+
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain settings.gradle[.kts])
513+
* @param {import('../index.js').Options} [opts={}]
514+
* @returns {Promise<string[]>} Paths to build.gradle[.kts] files (absolute)
515+
*/
516+
export async function discoverGradleSubprojects(workspaceRoot, opts = {}) {
517+
const root = path.resolve(workspaceRoot)
518+
const hasSettings = fs.existsSync(path.join(root, 'settings.gradle'))
519+
|| fs.existsSync(path.join(root, 'settings.gradle.kts'))
520+
521+
if (!hasSettings) {
522+
return []
523+
}
524+
525+
const gradleBin = resolveGradleBinary(root, opts)
526+
const manifestPaths = []
527+
528+
const rootBuildKts = path.join(root, 'build.gradle.kts')
529+
const rootBuild = path.join(root, 'build.gradle')
530+
if (fs.existsSync(rootBuildKts)) {
531+
manifestPaths.push(rootBuildKts)
532+
} else if (fs.existsSync(rootBuild)) {
533+
manifestPaths.push(rootBuild)
534+
}
535+
536+
const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${crypto.randomUUID()}.gradle`)
537+
try {
538+
fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT)
539+
let output
540+
try {
541+
output = invokeCommand(gradleBin, [
542+
'-q', '--no-daemon',
543+
'--init-script', initScriptPath,
544+
'daListProjects',
545+
], { cwd: root })
546+
} catch {
547+
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE]
548+
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
549+
}
550+
551+
const projects = parseGradleInitScriptOutput(output.toString())
552+
for (const proj of projects) {
553+
if (proj.path === ':') {
554+
continue
555+
}
556+
const projDir = path.resolve(proj.dir)
557+
const buildKts = path.join(projDir, 'build.gradle.kts')
558+
const buildGroovy = path.join(projDir, 'build.gradle')
559+
if (fs.existsSync(buildKts)) {
560+
manifestPaths.push(buildKts)
561+
} else if (fs.existsSync(buildGroovy)) {
562+
manifestPaths.push(buildGroovy)
563+
}
564+
}
565+
} finally {
566+
try { fs.unlinkSync(initScriptPath) } catch { /* ignore */ }
567+
}
568+
569+
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE]
570+
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
571+
}
572+
573+
/**
574+
* Parse the structured output from the Gradle init script.
575+
*
576+
* @param {string} raw - Raw stdout from gradle
577+
* @returns {{ path: string, dir: string }[]}
578+
*/
579+
export function parseGradleInitScriptOutput(raw) {
580+
const projects = []
581+
for (const rawLine of raw.split('\n')) {
582+
const line = rawLine.trimEnd()
583+
if (!line.startsWith('::DA_PROJECT::')) {
584+
continue
585+
}
586+
const prefix = '::DA_PROJECT::'
587+
const remainder = line.substring(prefix.length)
588+
const lastSep = remainder.lastIndexOf('::')
589+
if (lastSep < 0) {
590+
continue
591+
}
592+
const projPath = remainder.substring(0, lastSep)
593+
const dir = remainder.substring(lastSep + 2)
594+
if (projPath && dir) {
595+
projects.push({ path: projPath, dir })
596+
}
597+
}
598+
return projects
599+
}

src/workspace.js

Lines changed: 1 addition & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import fs from 'node:fs'
2-
import os from 'node:os'
32
import path from 'node:path'
43

54
import fg from 'fast-glob'
65
import { load as yamlLoad } from 'js-yaml'
76
import micromatch from 'micromatch'
87

9-
import { getCustom, getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from './tools.js'
8+
import { getCustom, getCustomPath, invokeCommand } from './tools.js'
109

1110
/** Default paths skipped during JS workspace discovery (merged with user patterns). */
1211
const DEFAULT_WORKSPACE_DISCOVERY_IGNORE = [
1312
'**/node_modules/**',
1413
'**/.git/**',
15-
'**/build/**',
16-
'**/.gradle/**',
1714
]
1815

1916
/**
@@ -227,159 +224,6 @@ async function discoverFromPackageJsonWorkspaces(root, packageJsonPath, globOpts
227224
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
228225
}
229226

230-
/**
231-
* Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
232-
*
233-
* @param {string} startDir - Absolute directory to start from
234-
* @param {string} wrapperName - Wrapper filename (e.g. `mvnw`)
235-
* @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
236-
* @returns {string | undefined}
237-
*/
238-
function traverseForWrapper(startDir, wrapperName, repoRoot = undefined) {
239-
const currentDir = path.resolve(startDir)
240-
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(currentDir).root
241-
const wrapperPath = path.join(currentDir, wrapperName)
242-
243-
try {
244-
fs.accessSync(wrapperPath, fs.constants.X_OK)
245-
return wrapperPath
246-
} catch (err) {
247-
if (err.code === 'ENOENT') {
248-
const rootDir = path.parse(currentDir).root
249-
if (currentDir === repoRoot || currentDir === rootDir) {
250-
return undefined
251-
}
252-
const parentDir = path.dirname(currentDir)
253-
if (parentDir === currentDir || parentDir === rootDir) {
254-
return undefined
255-
}
256-
return traverseForWrapper(parentDir, wrapperName, repoRoot)
257-
}
258-
throw new Error(`failure searching for ${wrapperName}`, { cause: err })
259-
}
260-
}
261-
262-
/** Gradle init script that emits structured project listing. */
263-
const GRADLE_INIT_SCRIPT = `allprojects {
264-
task daListProjects {
265-
doLast {
266-
println "::DA_PROJECT::\${project.path}::\${project.projectDir}"
267-
}
268-
}
269-
}
270-
`
271-
272-
/**
273-
* Resolve the Gradle binary, respecting wrapper preference.
274-
*
275-
* @param {string} startDir - Directory from which to start the wrapper search
276-
* @param {import('./index.js').Options} [opts={}]
277-
* @returns {string} Path to the Gradle binary
278-
*/
279-
function resolveGradleBinary(startDir, opts = {}) {
280-
const localWrapper = 'gradlew' + (process.platform === 'win32' ? '.bat' : '')
281-
const useWrapper = getWrapperPreference('gradle', opts)
282-
if (useWrapper) {
283-
const wrapper = traverseForWrapper(startDir, localWrapper)
284-
if (wrapper !== undefined) {
285-
return wrapper
286-
}
287-
}
288-
return getCustomPath('gradle', opts)
289-
}
290-
291-
/**
292-
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build.
293-
* Uses a custom init script to get structured project listing.
294-
*
295-
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain settings.gradle[.kts])
296-
* @param {import('./index.js').Options} [opts={}]
297-
* @returns {Promise<string[]>} Paths to build.gradle[.kts] files (absolute)
298-
*/
299-
export async function discoverGradleSubprojects(workspaceRoot, opts = {}) {
300-
const root = path.resolve(workspaceRoot)
301-
const hasSettings = fs.existsSync(path.join(root, 'settings.gradle'))
302-
|| fs.existsSync(path.join(root, 'settings.gradle.kts'))
303-
304-
if (!hasSettings) {
305-
return []
306-
}
307-
308-
const gradleBin = resolveGradleBinary(root, opts)
309-
const manifestPaths = []
310-
311-
const rootBuildKts = path.join(root, 'build.gradle.kts')
312-
const rootBuild = path.join(root, 'build.gradle')
313-
if (fs.existsSync(rootBuildKts)) {
314-
manifestPaths.push(rootBuildKts)
315-
} else if (fs.existsSync(rootBuild)) {
316-
manifestPaths.push(rootBuild)
317-
}
318-
319-
const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`)
320-
try {
321-
fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT)
322-
let output
323-
try {
324-
output = invokeCommand(gradleBin, [
325-
'-q', '--no-daemon',
326-
'--init-script', initScriptPath,
327-
'daListProjects',
328-
], { cwd: root })
329-
} catch {
330-
const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
331-
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
332-
}
333-
334-
const projects = parseGradleInitScriptOutput(output.toString())
335-
for (const proj of projects) {
336-
if (proj.path === ':') {
337-
continue
338-
}
339-
const projDir = path.resolve(proj.dir)
340-
const buildKts = path.join(projDir, 'build.gradle.kts')
341-
const buildGroovy = path.join(projDir, 'build.gradle')
342-
if (fs.existsSync(buildKts)) {
343-
manifestPaths.push(buildKts)
344-
} else if (fs.existsSync(buildGroovy)) {
345-
manifestPaths.push(buildGroovy)
346-
}
347-
}
348-
} finally {
349-
try { fs.unlinkSync(initScriptPath) } catch { /* ignore */ }
350-
}
351-
352-
const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
353-
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
354-
}
355-
356-
/**
357-
* Parse the structured output from the Gradle init script.
358-
*
359-
* @param {string} raw - Raw stdout from gradle
360-
* @returns {{ path: string, dir: string }[]}
361-
*/
362-
function parseGradleInitScriptOutput(raw) {
363-
const projects = []
364-
for (const line of raw.split('\n')) {
365-
if (!line.startsWith('::DA_PROJECT::')) {
366-
continue
367-
}
368-
const prefix = '::DA_PROJECT::'
369-
const remainder = line.substring(prefix.length)
370-
const lastSep = remainder.lastIndexOf('::')
371-
if (lastSep < 0) {
372-
continue
373-
}
374-
const projPath = remainder.substring(0, lastSep)
375-
const dir = remainder.substring(lastSep + 2)
376-
if (projPath && dir) {
377-
projects.push({ path: projPath, dir })
378-
}
379-
}
380-
return projects
381-
}
382-
383227
/**
384228
* Discover all Cargo.toml manifest paths in a Cargo workspace.
385229
* Uses `cargo metadata` to get workspace members.

0 commit comments

Comments
 (0)