|
| 1 | +# Multi-Module Detection & Project Tree Hierarchy |
| 2 | + |
| 3 | +> **Audience:** Open-source contributors and maintainers of `liberty-tools-vscode`. |
| 4 | +> **Purpose:** Explain how the extension finds Liberty projects, classifies them, and builds the tree shown in the Liberty Dashboard. |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +The extension scans every workspace folder for Maven (`pom.xml`) and Gradle (`build.gradle`) build files, checks each for the Liberty plugin, then wires parent-child relationships to produce a tree. Only projects that are Liberty-enabled (or that *contain* a Liberty-enabled descendant) appear in the dashboard. |
| 11 | + |
| 12 | +``` |
| 13 | +Workspace |
| 14 | +└── discoverWorkspace() ← entry point (projectDiscovery.ts) |
| 15 | + ├── Phase 1 · Read + Parse ← parallel file I/O |
| 16 | + ├── Phase 2 · Classify parents ← find aggregators + child paths |
| 17 | + ├── Phase 3 · Create LibertyProject ← shell objects into projectsMap |
| 18 | + ├── stampProjects() ← attach metadata (artifactId, flags) |
| 19 | + └── linkProjects() ← wire parent ↔ children, compute roots |
| 20 | +``` |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## Key Classes |
| 25 | + |
| 26 | +| Class / Interface | File | Role | |
| 27 | +|---|---|---| |
| 28 | +| `LibertyProject` | `src/liberty/libertyProject.ts` | VSCode `TreeItem`; holds state, terminal, parent/children refs | |
| 29 | +| `BaseLibertyProject` | `src/liberty/baseLibertyProject.ts` | Thin persistence DTO (label, path, contextValue) | |
| 30 | +| `BuildFileImpl` | `src/util/buildFile.ts` | Wraps validity + `contextValue` string for Maven | |
| 31 | +| `GradleBuildFile` | `src/util/buildFile.ts` | Extends `BuildFileImpl`; adds `children[]` (subproject names) | |
| 32 | +| `ProjectRegistry` | `src/liberty/projectRegistry.ts` | Singleton; owns `Map<path, LibertyProject>` + rootProjects list | |
| 33 | +| `ProjectTreeProvider` | `src/liberty/projectTreeProvider.ts` | VSCode `TreeDataProvider`; calls `discoverWorkspace`, feeds `ProjectRegistry` | |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## contextValue Scheme |
| 38 | + |
| 39 | +Every `LibertyProject` carries a `contextValue` string that drives which toolbar/context-menu commands are enabled in `package.json`. |
| 40 | + |
| 41 | +``` |
| 42 | +libertyProject:{tool}[:{variant}] |
| 43 | +``` |
| 44 | + |
| 45 | +| contextValue | Meaning | |
| 46 | +|---|---| |
| 47 | +| `libertyProject:maven` | Maven leaf project | |
| 48 | +| `libertyProject:gradle` | Gradle leaf project | |
| 49 | +| `libertyProject:maven:container` | Maven, Liberty plugin ≥ 3.3.0 (supports dev in container) | |
| 50 | +| `libertyProject:gradle:container` | Gradle, Liberty plugin ≥ 3.1.0 | |
| 51 | +| `libertyProject:maven:aggregator` | Maven parent POM (has `<modules>`) | |
| 52 | +| `libertyProject:gradle:aggregator` | Gradle root project (has `include` in `settings.gradle`) | |
| 53 | + |
| 54 | +Helper predicates in `src/definitions/constants.ts`: |
| 55 | + |
| 56 | +```ts |
| 57 | +isMaven(cv) // /^libertyProject:maven/ |
| 58 | +isGradle(cv) // /^libertyProject:gradle/ |
| 59 | +isContainer(cv) // /^libertyProject.*:container/ |
| 60 | +isAggregator(cv) // /^libertyProject.*:aggregator/ |
| 61 | +``` |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Discovery Pipeline (Step-by-Step) |
| 66 | + |
| 67 | +### Phase 1 — Parallel Read + Parse |
| 68 | + |
| 69 | +`discoverProjects()` in `projectDiscovery.ts` reads every build file **once** into a `ParsedBuildEntry[]`: |
| 70 | + |
| 71 | +- **Maven** – raw XML string stored; parsed later by `xml2js`. |
| 72 | +- **Gradle** – fast regex check (`LIBERTY_PLUGIN_ID_REGEX`) runs first. |
| 73 | + - If modern `plugins { id '...' }` syntax detected → `regexBuildFile` is set; **g2js skipped**. |
| 74 | + - If legacy `buildscript { dependencies {} }` syntax detected → `g2js.parseFile()` runs. |
| 75 | + - `settings.gradle` parsed in parallel when `include` blocks are found (needed for multi-module detection). |
| 76 | + |
| 77 | +### Phase 2 — Classify Parents |
| 78 | + |
| 79 | +Determines which build files are **aggregators** and records which paths are **child** build files. |
| 80 | + |
| 81 | +**Maven:** |
| 82 | +1. `mavenUtil.validParentPom(xmlString)` — checks `<build><plugins>` for `liberty-maven-plugin`. |
| 83 | +2. `mavenUtil.findChildMavenModules(xmlString)` — reads `<modules>` → builds `Map<parentArtifactId, string[]>`. |
| 84 | +3. Resolves each module name to an absolute `pom.xml` path → populates `mavenChildPomPaths`. |
| 85 | + |
| 86 | +**Gradle:** |
| 87 | +1. `gradleUtil.findChildGradleProjects(parsedBuild, parsedSettings, path)` — reads `include` from `settings.gradle`. |
| 88 | +2. Converts Gradle project notation (`:module`) to filesystem paths → populates `gradleChildBuildPaths`. |
| 89 | + |
| 90 | +### Phase 3 — Create Shell LibertyProject Objects |
| 91 | + |
| 92 | +Runs in parallel across all entries. Classification determines which validator to call: |
| 93 | + |
| 94 | +| Condition | Maven validator | Gradle validator | |
| 95 | +|---|---|---| |
| 96 | +| Is parent/aggregator | `validParentPom()` | `findChildGradleProjects()` | |
| 97 | +| Is known child | `BuildFileImpl(true, maven)` (always valid) | `validateGradleChildModule()` | |
| 98 | +| Standalone | `validPom(xml, childrenMap)` | `validGradleBuild()` / regex | |
| 99 | + |
| 100 | +Projects that fail validation are **skipped** (not added to `projectsMap`). |
| 101 | + |
| 102 | +If the project already exists in `existingProjects` (previous refresh), the existing object is reused to preserve terminal state; only label and `contextValue` are updated if changed. |
| 103 | + |
| 104 | +### stampProjects() |
| 105 | + |
| 106 | +Iterates `projectsMap` and extracts metadata from each `ParsedBuildEntry`: |
| 107 | + |
| 108 | +- **Maven** → `mavenUtil.extractMavenMetadata()`: |
| 109 | + - `artifactId`, `parentArtifactId`, `modules[]` |
| 110 | + - `isAggregator` (has `<modules>` or `<packaging>pom</packaging>`) |
| 111 | + - `isLibertyEnabled` (Liberty plugin present in `<build>` or a `<profile>`) |
| 112 | + |
| 113 | +- **Gradle** → `gradleUtil.extractGradleMetadata()`: |
| 114 | + - `projectName` (from `rootProject.name` in `settings.gradle`, else directory name) |
| 115 | + - `parentProjectName` (derived by scanning parent directory's `settings.gradle`) |
| 116 | + - `subprojects[]` (from `include` list) |
| 117 | + - `isAggregator`, `isLibertyEnabled` |
| 118 | + |
| 119 | +Stamps are written directly onto the `LibertyProject` object: |
| 120 | + |
| 121 | +```ts |
| 122 | +project.artifactId = metadata.artifactId; |
| 123 | +project.parentArtifactId = metadata.parentArtifactId; |
| 124 | +project.isAggregator = metadata.isAggregator; |
| 125 | +project.isLibertyEnabled = metadata.isLibertyEnabled; |
| 126 | +``` |
| 127 | + |
| 128 | +### linkProjects() |
| 129 | + |
| 130 | +Builds lookup maps and wires relationships: |
| 131 | + |
| 132 | +``` |
| 133 | +mavenProjectsByArtifactId : Map<artifactId, LibertyProject> |
| 134 | +gradleProjectsByName : Map<projectName, LibertyProject> |
| 135 | +``` |
| 136 | + |
| 137 | +**Parent-child wiring (pass 1 — by artifactId/name):** |
| 138 | +For each project with a `parentArtifactId`, look up the parent in the correct map, then: |
| 139 | +``` |
| 140 | +child.parent = parent |
| 141 | +parent.children.push(child) |
| 142 | +``` |
| 143 | +If child has no Liberty plugin but parent does → `child.isLibertyEnabled = true` (inheritance). |
| 144 | + |
| 145 | +**Parent-child wiring (pass 2 — by filesystem path, Maven only):** |
| 146 | +For each Maven aggregator, resolves `<module>` names to absolute pom paths. Catches cases where `artifactId`-based wiring missed a child (e.g., artifactId ≠ directory name). |
| 147 | + |
| 148 | +**Aggregator contextValue stamp:** |
| 149 | +```ts |
| 150 | +if (project.isAggregator) { |
| 151 | + project.setContextValue(isMaven(...) ? LIBERTY_PROJECT_MAVEN_AGGREGATOR |
| 152 | + : LIBERTY_PROJECT_GRADLE_AGGREGATOR); |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +**Root computation:** |
| 157 | +```ts |
| 158 | +rootProjects = projectsMap.values() |
| 159 | + .filter(p => !p.parent) // no parent = potential root |
| 160 | + .filter(p => p.isLibertyEnabled |
| 161 | + || hasLibertyDescendants(p)); // at least one Liberty node in subtree |
| 162 | +``` |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +## Tree Hierarchy — Visual Examples |
| 167 | + |
| 168 | +### Maven Multi-Module |
| 169 | + |
| 170 | +``` |
| 171 | +my-app-parent [libertyProject:maven:aggregator] |
| 172 | +├── server [libertyProject:maven:container] ← has liberty-maven-plugin ≥ 3.3.0 |
| 173 | +└── common [libertyProject:maven] ← inherits plugin from parent |
| 174 | +``` |
| 175 | + |
| 176 | +**How detected:** |
| 177 | +- `my-app-parent/pom.xml` has `<modules><module>server</module><module>common</module></modules>` → aggregator. |
| 178 | +- `server/pom.xml` has `liberty-maven-plugin` version ≥ 3.3.0 → container. |
| 179 | +- `common/pom.xml` has no plugin but its `<parent><artifactId>` matches → linked; inherits `isLibertyEnabled`. |
| 180 | + |
| 181 | +### Gradle Multi-Module |
| 182 | + |
| 183 | +``` |
| 184 | +my-gradle-root [libertyProject:gradle:aggregator] |
| 185 | +├── app [libertyProject:gradle:container] |
| 186 | +└── lib [libertyProject:gradle] |
| 187 | +``` |
| 188 | + |
| 189 | +**How detected:** |
| 190 | +- `my-gradle-root/settings.gradle` has `include 'app', 'lib'` → aggregator. |
| 191 | +- `app/build.gradle` has `id 'io.openliberty.tools.gradle.Liberty' version '3.8'` (≥ 3.1.0) → container. |
| 192 | +- `lib/build.gradle` has same plugin at lower version → leaf. |
| 193 | + |
| 194 | +### Single Project (No Multi-Module) |
| 195 | + |
| 196 | +``` |
| 197 | +my-service [libertyProject:maven] |
| 198 | +``` |
| 199 | + |
| 200 | +No parent, no children. Root project directly. |
| 201 | + |
| 202 | +### Aggregator with Non-Liberty Child |
| 203 | + |
| 204 | +``` |
| 205 | +platform [libertyProject:maven:aggregator] |
| 206 | +├── api (no Liberty plugin — hidden from dashboard) |
| 207 | +└── runtime [libertyProject:maven] |
| 208 | +``` |
| 209 | + |
| 210 | +`api` is linked in the internal tree but filtered out by: |
| 211 | +```ts |
| 212 | +// ProjectTreeProvider.getChildren() |
| 213 | +element.children.filter(child => |
| 214 | + child.isLibertyEnabled || this.hasLibertyDescendants(child) |
| 215 | +) |
| 216 | +``` |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Fallback: server.xml Discovery |
| 221 | + |
| 222 | +If a workspace folder has no `pom.xml` or `build.gradle` at the root but contains a `server.xml` at `src/main/liberty/config/server.xml`, the extension walks up 4 directories and checks for a build file there. This catches projects where the Liberty config is deeply nested. |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Progressive Refresh |
| 227 | + |
| 228 | +`discoverWorkspace()` processes workspace folders in parallel. After each folder completes, an `onFolderComplete` callback fires: |
| 229 | + |
| 230 | +```ts |
| 231 | +onFolderComplete(partial, foldersComplete, totalFolders) |
| 232 | +``` |
| 233 | + |
| 234 | +`ProjectTreeProvider` uses this to update the tree incrementally — the dashboard populates folder-by-folder instead of waiting for the full workspace scan. |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## Module Map |
| 239 | + |
| 240 | +``` |
| 241 | +src/ |
| 242 | +├── extension.ts ← activates extension, creates Registry + TreeProvider |
| 243 | +├── definitions/ |
| 244 | +│ └── constants.ts ← contextValue strings + helper predicates |
| 245 | +├── liberty/ |
| 246 | +│ ├── baseLibertyProject.ts ← persistence DTO |
| 247 | +│ ├── libertyProject.ts ← TreeItem model (parent/children/state) |
| 248 | +│ ├── projectDiscovery.ts ← discoverWorkspace() + 3-phase pipeline |
| 249 | +│ ├── projectRegistry.ts ← singleton registry (source of truth) |
| 250 | +│ ├── projectTreeProvider.ts ← TreeDataProvider + refresh + pickProject() |
| 251 | +│ ├── dashboard.ts ← workspace storage schema (DashboardData) |
| 252 | +│ └── devCommands.ts ← Liberty dev commands (start/stop/test) |
| 253 | +└── util/ |
| 254 | + ├── buildFile.ts ← BuildFileImpl / GradleBuildFile |
| 255 | + ├── mavenUtil.ts ← pom.xml parsing, metadata extraction |
| 256 | + ├── gradleUtil.ts ← build.gradle parsing, settings.gradle, metadata |
| 257 | + └── helperUtil.ts ← filterProjects(), storage helpers |
| 258 | +``` |
| 259 | + |
| 260 | +--- |
| 261 | + |
| 262 | +## Data Flow Summary |
| 263 | + |
| 264 | +``` |
| 265 | +vscode.workspace.findFiles() |
| 266 | + │ pom.xml / build.gradle paths |
| 267 | + ▼ |
| 268 | +discoverProjects() ← Phase 1: parse all files in parallel |
| 269 | + │ ParsedBuildEntry[] |
| 270 | + ▼ |
| 271 | +[Phase 2: classify] ← find aggregators, resolve child paths |
| 272 | + │ |
| 273 | + ▼ |
| 274 | +[Phase 3: create objects] ← validate + instantiate LibertyProject shells |
| 275 | + │ Map<path, LibertyProject> |
| 276 | + ▼ |
| 277 | +stampProjects() ← attach artifactId, isAggregator, isLibertyEnabled |
| 278 | + │ MavenMetadataMap + GradleMetadataMap |
| 279 | + ▼ |
| 280 | +linkProjects() ← wire parent ↔ child, compute rootProjects[] |
| 281 | + │ |
| 282 | + ▼ |
| 283 | +ProjectRegistry ← setProjects() + setRootProjects() |
| 284 | + │ |
| 285 | + ▼ |
| 286 | +ProjectTreeProvider ← getChildren() renders dashboard tree |
| 287 | +``` |
0 commit comments