@versu/core 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +241 -0
  2. package/dist/adapters/gradle/constants.d.ts +13 -0
  3. package/dist/adapters/gradle/constants.d.ts.map +1 -0
  4. package/dist/adapters/gradle/constants.js +12 -0
  5. package/dist/adapters/gradle/gradle-project-information.d.ts +19 -0
  6. package/dist/adapters/gradle/gradle-project-information.d.ts.map +1 -0
  7. package/dist/adapters/gradle/gradle-project-information.js +233 -0
  8. package/dist/adapters/gradle/init-project-information.gradle.kts +163 -0
  9. package/dist/adapters/gradle/services/gradle-adapter-identifier.d.ts +21 -0
  10. package/dist/adapters/gradle/services/gradle-adapter-identifier.d.ts.map +1 -0
  11. package/dist/adapters/gradle/services/gradle-adapter-identifier.js +44 -0
  12. package/dist/adapters/gradle/services/gradle-module-detector.d.ts +19 -0
  13. package/dist/adapters/gradle/services/gradle-module-detector.d.ts.map +1 -0
  14. package/dist/adapters/gradle/services/gradle-module-detector.js +28 -0
  15. package/dist/adapters/gradle/services/gradle-module-system-factory.d.ts +26 -0
  16. package/dist/adapters/gradle/services/gradle-module-system-factory.d.ts.map +1 -0
  17. package/dist/adapters/gradle/services/gradle-module-system-factory.js +29 -0
  18. package/dist/adapters/gradle/services/gradle-version-update-strategy.d.ts +23 -0
  19. package/dist/adapters/gradle/services/gradle-version-update-strategy.d.ts.map +1 -0
  20. package/dist/adapters/gradle/services/gradle-version-update-strategy.js +38 -0
  21. package/dist/adapters/project-information.d.ts +62 -0
  22. package/dist/adapters/project-information.d.ts.map +1 -0
  23. package/dist/adapters/project-information.js +1 -0
  24. package/dist/changelog/index.d.ts +14 -0
  25. package/dist/changelog/index.d.ts.map +1 -0
  26. package/dist/changelog/index.js +132 -0
  27. package/dist/config/index.d.ts +122 -0
  28. package/dist/config/index.d.ts.map +1 -0
  29. package/dist/config/index.js +117 -0
  30. package/dist/factories/adapter-identifier-registry.d.ts +12 -0
  31. package/dist/factories/adapter-identifier-registry.d.ts.map +1 -0
  32. package/dist/factories/adapter-identifier-registry.js +24 -0
  33. package/dist/factories/module-system-factory.d.ts +10 -0
  34. package/dist/factories/module-system-factory.d.ts.map +1 -0
  35. package/dist/factories/module-system-factory.js +18 -0
  36. package/dist/git/index.d.ts +355 -0
  37. package/dist/git/index.d.ts.map +1 -0
  38. package/dist/git/index.js +702 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +22 -0
  42. package/dist/semver/index.d.ts +86 -0
  43. package/dist/semver/index.d.ts.map +1 -0
  44. package/dist/semver/index.js +186 -0
  45. package/dist/services/adapter-identifier-registry.d.ts +38 -0
  46. package/dist/services/adapter-identifier-registry.d.ts.map +1 -0
  47. package/dist/services/adapter-identifier-registry.js +59 -0
  48. package/dist/services/adapter-identifier.d.ts +31 -0
  49. package/dist/services/adapter-identifier.d.ts.map +1 -0
  50. package/dist/services/adapter-identifier.js +1 -0
  51. package/dist/services/adapter-metadata-provider.d.ts +51 -0
  52. package/dist/services/adapter-metadata-provider.d.ts.map +1 -0
  53. package/dist/services/adapter-metadata-provider.js +66 -0
  54. package/dist/services/changelog-generator.d.ts +16 -0
  55. package/dist/services/changelog-generator.d.ts.map +1 -0
  56. package/dist/services/changelog-generator.js +28 -0
  57. package/dist/services/commit-analyzer.d.ts +47 -0
  58. package/dist/services/commit-analyzer.d.ts.map +1 -0
  59. package/dist/services/commit-analyzer.js +89 -0
  60. package/dist/services/configuration-loader.d.ts +23 -0
  61. package/dist/services/configuration-loader.d.ts.map +1 -0
  62. package/dist/services/configuration-loader.js +79 -0
  63. package/dist/services/configuration-validator.d.ts +16 -0
  64. package/dist/services/configuration-validator.d.ts.map +1 -0
  65. package/dist/services/configuration-validator.js +24 -0
  66. package/dist/services/git-operations.d.ts +16 -0
  67. package/dist/services/git-operations.d.ts.map +1 -0
  68. package/dist/services/git-operations.js +89 -0
  69. package/dist/services/module-detector.d.ts +24 -0
  70. package/dist/services/module-detector.d.ts.map +1 -0
  71. package/dist/services/module-detector.js +1 -0
  72. package/dist/services/module-registry.d.ts +45 -0
  73. package/dist/services/module-registry.d.ts.map +1 -0
  74. package/dist/services/module-registry.js +57 -0
  75. package/dist/services/module-system-factory.d.ts +27 -0
  76. package/dist/services/module-system-factory.d.ts.map +1 -0
  77. package/dist/services/module-system-factory.js +1 -0
  78. package/dist/services/version-applier.d.ts +26 -0
  79. package/dist/services/version-applier.d.ts.map +1 -0
  80. package/dist/services/version-applier.js +63 -0
  81. package/dist/services/version-bumper.d.ts +159 -0
  82. package/dist/services/version-bumper.d.ts.map +1 -0
  83. package/dist/services/version-bumper.js +291 -0
  84. package/dist/services/version-manager.d.ts +68 -0
  85. package/dist/services/version-manager.d.ts.map +1 -0
  86. package/dist/services/version-manager.js +94 -0
  87. package/dist/services/version-update-strategy.d.ts +18 -0
  88. package/dist/services/version-update-strategy.d.ts.map +1 -0
  89. package/dist/services/version-update-strategy.js +1 -0
  90. package/dist/services/versu-runner.d.ts +46 -0
  91. package/dist/services/versu-runner.d.ts.map +1 -0
  92. package/dist/services/versu-runner.js +188 -0
  93. package/dist/utils/banner.d.ts +2 -0
  94. package/dist/utils/banner.d.ts.map +1 -0
  95. package/dist/utils/banner.js +12 -0
  96. package/dist/utils/commits.d.ts +12 -0
  97. package/dist/utils/commits.d.ts.map +1 -0
  98. package/dist/utils/commits.js +24 -0
  99. package/dist/utils/file.d.ts +7 -0
  100. package/dist/utils/file.d.ts.map +1 -0
  101. package/dist/utils/file.js +19 -0
  102. package/dist/utils/index.d.ts +6 -0
  103. package/dist/utils/index.d.ts.map +1 -0
  104. package/dist/utils/index.js +5 -0
  105. package/dist/utils/logger.d.ts +14 -0
  106. package/dist/utils/logger.d.ts.map +1 -0
  107. package/dist/utils/logger.js +22 -0
  108. package/dist/utils/properties.d.ts +24 -0
  109. package/dist/utils/properties.d.ts.map +1 -0
  110. package/dist/utils/properties.js +94 -0
  111. package/dist/utils/version.d.ts +3 -0
  112. package/dist/utils/version.d.ts.map +1 -0
  113. package/dist/utils/version.js +4 -0
  114. package/dist/utils/versioning.d.ts +9 -0
  115. package/dist/utils/versioning.d.ts.map +1 -0
  116. package/dist/utils/versioning.js +20 -0
  117. package/package.json +73 -0
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @versu/core - Core Library
2
+
3
+ The core business logic powering VERSU (Version Engine for Repo Semantic Evolution). This package is completely framework-agnostic and can be integrated into any TypeScript/JavaScript project, CI/CD system, or custom tooling.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @versu/core
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { VersuRunner } from '@versu/core';
15
+
16
+ const runner = new VersuRunner({
17
+ repoRoot: '/path/to/repository',
18
+ adapter: 'gradle', // Optional - auto-detected if not specified
19
+ dryRun: false,
20
+ pushTags: true,
21
+ pushChanges: true,
22
+ generateChangelog: true
23
+ });
24
+
25
+ const result = await runner.run();
26
+
27
+ console.log(`Bumped: ${result.bumped}`);
28
+ console.log(`Changed modules:`, result.changedModules);
29
+ console.log(`Created tags:`, result.createdTags);
30
+ ```
31
+
32
+ For detailed pre-release configuration and examples, see [PRERELEASE.md](./PRERELEASE.md).
33
+
34
+ ## VersuRunner API
35
+
36
+ ### Options
37
+
38
+ ```typescript
39
+ interface RunnerOptions {
40
+ repoRoot: string; // Path to repository root
41
+ adapter?: string; // Language adapter (auto-detected if not specified)
42
+ dryRun?: boolean; // Preview changes without writing (default: false)
43
+ pushTags?: boolean; // Push version tags (default: true)
44
+ prereleaseMode?: boolean; // Generate pre-release versions (default: false)
45
+ prereleaseId?: string; // Pre-release identifier: alpha, beta, rc, etc. (default: 'alpha')
46
+ bumpUnchanged?: boolean; // Bump modules with no changes in prerelease mode (default: false)
47
+ addBuildMetadata?: boolean; // Add build metadata with short SHA (default: false)
48
+ timestampVersions?: boolean; // Use timestamp-based prerelease IDs (default: false)
49
+ appendSnapshot?: boolean; // Add -SNAPSHOT suffix (Gradle only) (default: false)
50
+ pushChanges?: boolean; // Commit and push version changes (default: true)
51
+ generateChangelog?: boolean; // Generate changelogs (default: true)
52
+ }
53
+ ```
54
+
55
+ ### Result
56
+
57
+ ```typescript
58
+ interface RunnerResult {
59
+ bumped: boolean; // Whether any version was updated
60
+ changedModules: Array<{
61
+ name: string; // Module name
62
+ from: string; // Previous version
63
+ to: string; // New version
64
+ }>;
65
+ createdTags: string[]; // Git tags that were created
66
+ changelogPaths: string[]; // Generated changelog file paths
67
+ }
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ VERSU core uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) for configuration loading and [Zod](https://github.com/colinhacks/zod) for validation.
73
+
74
+ ### Supported Configuration Files
75
+
76
+ 1. `package.json` (in a `"versu"` property)
77
+ 2. `.versurc.json`
78
+ 3. `.versurc.yaml` / `.versurc.yml`
79
+ 4. `.versurc.js` or `versu.config.js` (JavaScript)
80
+
81
+ ### Configuration Example
82
+
83
+ ```json
84
+ {
85
+ "defaultBump": "patch",
86
+ "commitTypes": {
87
+ "feat": "minor",
88
+ "fix": "patch",
89
+ "perf": "patch",
90
+ "docs": "ignore"
91
+ },
92
+ "dependencyRules": {
93
+ "onMajorOfDependency": "minor",
94
+ "onMinorOfDependency": "patch",
95
+ "onPatchOfDependency": "none"
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## Adapters
101
+
102
+ ### Gradle Adapter
103
+
104
+ Built-in support for Gradle projects (Groovy & Kotlin DSL).
105
+
106
+ **Features:**
107
+
108
+ - Multi-module project detection
109
+ - Version management through root `gradle.properties`
110
+ - Dependency detection
111
+ - Both Groovy and Kotlin DSL support
112
+
113
+ **Version Format:**
114
+
115
+ ```properties
116
+ # Root module
117
+ version=1.0.0
118
+
119
+ # Submodules
120
+ core.version=2.1.0
121
+ api.version=1.5.0
122
+ ```
123
+
124
+ ### Creating Custom Adapters
125
+
126
+ To add support for new project types, implement a language adapter following the pattern in `src/adapters/gradle/`:
127
+
128
+ ```typescript
129
+ import {
130
+ AdapterIdentifier,
131
+ ModuleDetector,
132
+ VersionUpdateStrategy,
133
+ ModuleSystemFactory,
134
+ AdapterMetadata,
135
+ RawProjectInformation,
136
+ ProcessedModuleChange
137
+ } from '@versu/core';
138
+
139
+ // 1. Adapter identifier for auto-detection
140
+ class MyAdapterIdentifier implements AdapterIdentifier {
141
+ readonly metadata: AdapterMetadata = {
142
+ id: 'my-adapter',
143
+ capabilities: { supportsSnapshots: false }
144
+ };
145
+
146
+ async accept(projectRoot: string): Promise<boolean> {
147
+ // Check for adapter-specific files
148
+ return await fileExists(path.join(projectRoot, 'my-build-file'));
149
+ }
150
+ }
151
+
152
+ // 2. Module detector for discovering project structure
153
+ class MyModuleDetector implements ModuleDetector {
154
+ async detectModules(projectRoot: string): Promise<RawProjectInformation> {
155
+ // Discover and return modules and dependencies
156
+ return {
157
+ modules: [
158
+ { name: 'root', path: projectRoot, version: '1.0.0' },
159
+ { name: 'module-a', path: join(projectRoot, 'module-a'), version: '2.0.0' }
160
+ ],
161
+ dependencies: [
162
+ { from: 'module-a', to: 'root' }
163
+ ]
164
+ };
165
+ }
166
+ }
167
+
168
+ // 3. Version update strategy for applying changes
169
+ class MyVersionUpdateStrategy implements VersionUpdateStrategy {
170
+ async applyVersionUpdates(
171
+ changes: ProcessedModuleChange[],
172
+ projectRoot: string
173
+ ): Promise<void> {
174
+ // Apply version changes to build files
175
+ for (const change of changes) {
176
+ // Update version in your project's format
177
+ }
178
+ }
179
+ }
180
+
181
+ // 4. Module system factory to tie it all together
182
+ class MyModuleSystemFactory implements ModuleSystemFactory {
183
+ createDetector(): ModuleDetector {
184
+ return new MyModuleDetector();
185
+ }
186
+
187
+ createVersionUpdateStrategy(): VersionUpdateStrategy {
188
+ return new MyVersionUpdateStrategy();
189
+ }
190
+ }
191
+ ```
192
+
193
+ Then register your adapter:
194
+
195
+ 1. Add to `src/factories/module-system-factory.ts` in the `createModuleSystemFactory` function
196
+ 2. Add to `src/services/adapter-identifier-registry.ts` in the registry initialization
197
+
198
+ ## Development
199
+
200
+ ### Building
201
+
202
+ ```bash
203
+ # From monorepo root
204
+ npm run build
205
+
206
+ # Or from core package
207
+ cd packages/core
208
+ npm run build
209
+ ```
210
+
211
+ ### Testing
212
+
213
+ ```bash
214
+ # From monorepo root
215
+ npm test
216
+
217
+ # Or from core package
218
+ cd packages/core
219
+ npm test
220
+ npm run test:coverage
221
+ ```
222
+
223
+ ### Publishing
224
+
225
+ ```bash
226
+ npm publish --workspace packages/core --access public
227
+ ```
228
+
229
+ ## Related Packages
230
+
231
+ - **[@versu/cli](../cli)** - Command-line interface
232
+ - **[@versu/action](../action)** - GitHub Actions integration
233
+
234
+ ## Requirements
235
+
236
+ - **Node.js**: >= 20
237
+ - **TypeScript**: >= 5.0 (if using TypeScript)
238
+
239
+ ## License
240
+
241
+ MIT License - see [LICENSE](../../LICENSE) for details.
@@ -0,0 +1,13 @@
1
+ /** Standard filename for Gradle project properties file ('gradle.properties'). */
2
+ export declare const GRADLE_PROPERTIES_FILE = "gradle.properties";
3
+ /** Standard filename for Gradle build script using Groovy DSL ('build.gradle'). */
4
+ export declare const GRADLE_BUILD_FILE = "build.gradle";
5
+ /** Standard filename for Gradle build script using Kotlin DSL ('build.gradle.kts'). */
6
+ export declare const GRADLE_BUILD_KTS_FILE = "build.gradle.kts";
7
+ /** Standard filename for Gradle settings file using Groovy DSL ('settings.gradle'). */
8
+ export declare const GRADLE_SETTINGS_FILE = "settings.gradle";
9
+ /** Standard filename for Gradle settings file using Kotlin DSL ('settings.gradle.kts'). */
10
+ export declare const GRADLE_SETTINGS_KTS_FILE = "settings.gradle.kts";
11
+ /** Unique identifier for the Gradle adapter ('gradle'). */
12
+ export declare const GRADLE_ID = "gradle";
13
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/adapters/gradle/constants.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD,uFAAuF;AACvF,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D,2DAA2D;AAC3D,eAAO,MAAM,SAAS,WAAW,CAAC"}
@@ -0,0 +1,12 @@
1
+ /** Standard filename for Gradle project properties file ('gradle.properties'). */
2
+ export const GRADLE_PROPERTIES_FILE = 'gradle.properties';
3
+ /** Standard filename for Gradle build script using Groovy DSL ('build.gradle'). */
4
+ export const GRADLE_BUILD_FILE = 'build.gradle';
5
+ /** Standard filename for Gradle build script using Kotlin DSL ('build.gradle.kts'). */
6
+ export const GRADLE_BUILD_KTS_FILE = 'build.gradle.kts';
7
+ /** Standard filename for Gradle settings file using Groovy DSL ('settings.gradle'). */
8
+ export const GRADLE_SETTINGS_FILE = 'settings.gradle';
9
+ /** Standard filename for Gradle settings file using Kotlin DSL ('settings.gradle.kts'). */
10
+ export const GRADLE_SETTINGS_KTS_FILE = 'settings.gradle.kts';
11
+ /** Unique identifier for the Gradle adapter ('gradle'). */
12
+ export const GRADLE_ID = 'gradle';
@@ -0,0 +1,19 @@
1
+ import { ProjectInformation, RawProjectInformation } from '../project-information.js';
2
+ /**
3
+ * Executes Gradle to collect raw project structure information.
4
+ * Runs gradlew with init script to output JSON containing module hierarchy, versions, and dependencies.
5
+ * @param projectRoot - Absolute path to the Gradle project root directory
6
+ * @param outputFile - Path to output JSON file to be generated
7
+ * @returns Promise resolving to raw project information as JSON
8
+ * @throws {Error} If initialization script not found or Gradle execution fails
9
+ */
10
+ export declare function getRawProjectInformation(projectRoot: string, outputFile: string): Promise<RawProjectInformation>;
11
+ /**
12
+ * Transforms raw project information into structured, queryable format.
13
+ * Normalizes modules, identifies root, parses versions, and maps dependencies.
14
+ * @param projectInformation - Raw project information from Gradle
15
+ * @returns Structured ProjectInformation with normalized data
16
+ * @throws {Error} If no root module found in hierarchy
17
+ */
18
+ export declare function getProjectInformation(projectInformation: RawProjectInformation): ProjectInformation;
19
+ //# sourceMappingURL=gradle-project-information.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradle-project-information.d.ts","sourceRoot":"","sources":["../../../src/adapters/gradle/gradle-project-information.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AA4H1G;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuEtH;AA2CD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,qBAAqB,GAAG,kBAAkB,CA6CnG"}
@@ -0,0 +1,233 @@
1
+ import path, { join } from 'path';
2
+ import { createInitialVersion, parseSemVer } from '../../semver/index.js';
3
+ import { exists } from '../../utils/file.js';
4
+ import { fileURLToPath } from 'url';
5
+ import { execa } from 'execa';
6
+ import fs from 'fs/promises';
7
+ import crypto from 'crypto';
8
+ import fg from 'fast-glob';
9
+ import { parseProperties } from '../../utils/properties.js';
10
+ import { logger } from '../../utils/logger.js';
11
+ /**
12
+ * Name of the Gradle wrapper script file.
13
+ * Ensures consistent builds without requiring pre-installed Gradle.
14
+ */
15
+ const GRADLE_WRAPPER = 'gradlew';
16
+ /**
17
+ * Relative path to the Gradle initialization script within the action.
18
+ * Injected into Gradle to collect project structure information as JSON.
19
+ */
20
+ const GRADLE_INIT_SCRIPT = './init-project-information.gradle.kts';
21
+ /**
22
+ * Finds all Gradle build files recursively under the project root.
23
+ * Searches for settings.gradle, settings.gradle.kts, build.gradle, and build.gradle.kts files.
24
+ * @param projectRoot - Absolute path to the Gradle project root directory
25
+ * @returns Promise resolving to array of relative paths to Gradle build files
26
+ */
27
+ async function findGradleFiles(projectRoot) {
28
+ const patterns = [
29
+ '**/settings.gradle',
30
+ '**/settings.gradle.kts',
31
+ '**/build.gradle',
32
+ '**/build.gradle.kts'
33
+ ];
34
+ const files = await fg(patterns, {
35
+ cwd: projectRoot,
36
+ absolute: false,
37
+ ignore: ['**/node_modules/**', '**/build/**', '**/.gradle/**']
38
+ });
39
+ // Sort for consistent ordering
40
+ return files.sort();
41
+ }
42
+ /**
43
+ * Computes SHA-256 hash of all Gradle build files.
44
+ * Used to detect changes in project configuration that would invalidate cached information.
45
+ * @param projectRoot - Absolute path to the Gradle project root directory
46
+ * @returns Promise resolving to hexadecimal hash string
47
+ */
48
+ async function computeGradleFilesHash(projectRoot) {
49
+ const files = await findGradleFiles(projectRoot);
50
+ const hash = crypto.createHash('sha256');
51
+ for (const file of files) {
52
+ const content = await fs.readFile(join(projectRoot, file), 'utf-8');
53
+ hash.update(file); // Include file path for uniqueness
54
+ hash.update(content);
55
+ }
56
+ return hash.digest('hex');
57
+ }
58
+ /**
59
+ * Executes the Gradle wrapper script to generate project information.
60
+ * Runs gradlew with initialization script to create the project-information.json file.
61
+ * @param projectRoot - Absolute path to the Gradle project root directory
62
+ * @param outputFile - Path to output JSON file to be generated
63
+ * @throws {Error} If initialization script not found or Gradle execution fails
64
+ */
65
+ async function executeGradleScript(projectRoot, outputFile) {
66
+ logger.info(`⚙️ Executing Gradle to collect project information...`);
67
+ const gradlew = join(projectRoot, GRADLE_WRAPPER);
68
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
69
+ const initScriptPath = join(dirname, GRADLE_INIT_SCRIPT);
70
+ // Check if init script exists
71
+ const scriptExists = await exists(initScriptPath);
72
+ if (!scriptExists) {
73
+ throw new Error(`Init script not found at ${initScriptPath}. ` +
74
+ `Please create the ${GRADLE_INIT_SCRIPT} file.`);
75
+ }
76
+ // Prepare Gradle command arguments
77
+ const args = [
78
+ '--quiet', // Suppress non-error output for clean JSON
79
+ '--console=plain', // Disable ANSI formatting
80
+ '--init-script', // Inject initialization script
81
+ initScriptPath,
82
+ 'structure', // Custom task that outputs project structure
83
+ `-PprojectInfoOutput=${outputFile}`
84
+ ];
85
+ // Execute Gradle wrapper with the prepared arguments
86
+ const result = await execa(gradlew, args, {
87
+ cwd: projectRoot, // Run from project root
88
+ reject: false // Handle non-zero exit codes ourselves
89
+ });
90
+ // Check for Gradle execution failure
91
+ if (result.exitCode !== 0) {
92
+ throw new Error(`Gradle command failed with exit code ${result.exitCode}: ${result.stderr}`);
93
+ }
94
+ logger.info(`✅ Gradle project information generated at ${outputFile}.`);
95
+ }
96
+ /**
97
+ * Executes Gradle to collect raw project structure information.
98
+ * Runs gradlew with init script to output JSON containing module hierarchy, versions, and dependencies.
99
+ * @param projectRoot - Absolute path to the Gradle project root directory
100
+ * @param outputFile - Path to output JSON file to be generated
101
+ * @returns Promise resolving to raw project information as JSON
102
+ * @throws {Error} If initialization script not found or Gradle execution fails
103
+ */
104
+ export async function getRawProjectInformation(projectRoot, outputFile) {
105
+ // Step 1: Check if project-information.json exists
106
+ const fileExists = await exists(outputFile);
107
+ let data = {};
108
+ let executeScript = true;
109
+ // Compute hash of all Gradle build files
110
+ const currentHash = await computeGradleFilesHash(projectRoot);
111
+ logger.debug(`🔍 Computed Gradle files hash: ${currentHash}`);
112
+ if (fileExists) {
113
+ logger.info(`💾 Cached project information found at ${outputFile}. Validating cache...`);
114
+ // Step 2: File exists, check cache validity
115
+ try {
116
+ const fileContent = await fs.readFile(outputFile, 'utf-8');
117
+ const cachedData = JSON.parse(fileContent);
118
+ // Step 2.1: Compare hashes
119
+ if (cachedData.hash === currentHash) {
120
+ logger.info(`✅ Cache is valid. Using cached project information.`);
121
+ // Cache hit - use cached data
122
+ executeScript = false;
123
+ data = cachedData.data;
124
+ }
125
+ else {
126
+ logger.debug(`❌ Cache is invalid. Cached hash: ${cachedData.hash}`);
127
+ logger.info(`🔄 Gradle files changed, regenerating project information...`);
128
+ }
129
+ // Cache miss - hash mismatch, need to regenerate
130
+ }
131
+ catch (error) {
132
+ // If there's any error reading/parsing cached file, regenerate
133
+ logger.warning(`⚠️ Failed to read cached project information: ${error}`);
134
+ }
135
+ }
136
+ if (executeScript) {
137
+ // Step 3: File doesn't exist or cache is invalid - execute Gradle script
138
+ const outputFile = join(projectRoot, 'build', 'project-information.json');
139
+ await executeGradleScript(projectRoot, outputFile);
140
+ // Verify that the output file was created
141
+ const fileExistsAfterExec = await exists(outputFile);
142
+ if (!fileExistsAfterExec) {
143
+ throw new Error(`Expected output file not found at ${outputFile}. ` +
144
+ `Ensure that the Gradle init script is correctly generating the project information.`);
145
+ }
146
+ // Read the output file content
147
+ const fileContent = await fs.readFile(outputFile, 'utf-8');
148
+ // Parse JSON output from Gradle
149
+ data = JSON.parse(fileContent.trim() || '{}');
150
+ }
151
+ // Compute hash and save with cache information
152
+ const cachedData = {
153
+ hash: currentHash,
154
+ data
155
+ };
156
+ // Read gradle.properites and add version
157
+ const projectInformation = await getInformationWithVersions(projectRoot, data);
158
+ if (executeScript) {
159
+ // Write back to file with hash for future cache validation
160
+ await fs.writeFile(outputFile, JSON.stringify(cachedData, null, 2), 'utf-8');
161
+ }
162
+ return projectInformation;
163
+ }
164
+ /**
165
+ * Reads gradle.properties to extract module versions and augment raw project information.
166
+ * @param projectRoot - Absolute path to the Gradle project root directory
167
+ * @param projectInformation - Gradle project information without versions
168
+ * @returns Promise resolving to augmented RawProjectInformation with versions
169
+ */
170
+ async function getInformationWithVersions(projectRoot, projectInformation) {
171
+ const gradlePropertiesFile = join(projectRoot, 'gradle.properties');
172
+ const gradlePropertiesExists = await exists(gradlePropertiesFile);
173
+ const result = {};
174
+ let moduleVersions = new Map();
175
+ if (gradlePropertiesExists) {
176
+ moduleVersions = await parseProperties(gradlePropertiesFile);
177
+ for (const [moduleId, module] of Object.entries(projectInformation)) {
178
+ const version = moduleVersions.get(module.versionProperty);
179
+ const resultVersion = version ? version : undefined;
180
+ result[moduleId] = {
181
+ ...module,
182
+ version: resultVersion,
183
+ declaredVersion: resultVersion !== undefined
184
+ };
185
+ }
186
+ }
187
+ return result;
188
+ }
189
+ /**
190
+ * Transforms raw project information into structured, queryable format.
191
+ * Normalizes modules, identifies root, parses versions, and maps dependencies.
192
+ * @param projectInformation - Raw project information from Gradle
193
+ * @returns Structured ProjectInformation with normalized data
194
+ * @throws {Error} If no root module found in hierarchy
195
+ */
196
+ export function getProjectInformation(projectInformation) {
197
+ const moduleIds = Object.keys(projectInformation);
198
+ const modules = new Map();
199
+ // Find root module by looking for the one with type 'root'
200
+ let rootModule;
201
+ for (const [moduleId, rawModule] of Object.entries(projectInformation)) {
202
+ if (rawModule.type === 'root') {
203
+ rootModule = moduleId;
204
+ }
205
+ // Create normalized Module object
206
+ const module = {
207
+ id: moduleId,
208
+ name: rawModule.name,
209
+ path: rawModule.path,
210
+ type: rawModule.type,
211
+ affectedModules: new Set(rawModule.affectedModules),
212
+ // Parse version if present, otherwise create initial version
213
+ version: rawModule.version === undefined ?
214
+ createInitialVersion() :
215
+ parseSemVer(rawModule.version),
216
+ declaredVersion: rawModule.declaredVersion,
217
+ };
218
+ if ('versionProperty' in rawModule) {
219
+ module['versionProperty'] = rawModule.versionProperty;
220
+ }
221
+ modules.set(moduleId, module);
222
+ }
223
+ // Validate that a root module was found
224
+ if (!rootModule) {
225
+ throw new Error('No root module found in hierarchy. ' +
226
+ 'Every project hierarchy must contain exactly one module with type "root".');
227
+ }
228
+ return {
229
+ moduleIds,
230
+ modules,
231
+ rootModule
232
+ };
233
+ }