Skip to content

Commit c319ba4

Browse files
authored
Merge pull request #598 from dshimo/multi-module-support
Multi module enhancements
2 parents 723b080 + 6af5db9 commit c319ba4

24 files changed

Lines changed: 3143 additions & 1471 deletions

.mocharc.unit.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"require": ["out/test/unit/setup.js"],
3+
"spec": "out/test/unit/**/*.test.js",
4+
"timeout": 5000
5+
}

.vscode/launch.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
"request": "launch",
1212
"runtimeExecutable": "${execPath}",
1313
"args": [
14-
"--extensionDevelopmentPath=${workspaceFolder}"
14+
"--extensionDevelopmentPath=${workspaceFolder}",
15+
"--disable-extension=open-liberty.liberty-dev-vscode-ext"
1516
],
1617
"outFiles": [
17-
"${workspaceFolder}/dist/**/*.js"
18-
]
19-
// "preLaunchTask": "npm: watch"
18+
"${workspaceFolder}/out/**/*.js"
19+
],
20+
"preLaunchTask": "npm: watch"
2021
},
2122
{
2223
"name": "Extension Tests",
@@ -29,7 +30,7 @@
2930
"${workspaceFolder}/src/test/myProject"
3031
],
3132
"outFiles": [
32-
"${workspaceFolder}/out/test/**/*.js"
33+
"${workspaceFolder}/out/**/*.js"
3334
],
3435
"preLaunchTask": "npm: watch"
3536
}

.vscode/tasks.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
"owner": "typescript",
1111
"pattern": [
1212
{
13-
"regexp": "\\[tsl\\] ERROR",
13+
"regexp": "ERROR in",
1414
"file": 1,
1515
"location": 2,
1616
"message": 3
1717
}
1818
],
1919
"background": {
2020
"activeOnStart": true,
21-
"beginsPattern": "Compilation \\w+ starting…",
22-
"endsPattern": "Compilation\\s+finished"
21+
"beginsPattern": "asset",
22+
"endsPattern": "compiled"
2323
}
2424
},
2525
"isBackground": true,

docs/multi-module-detection.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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

Comments
 (0)