Skip to content

Commit f9979d5

Browse files
committed
Show project dependencies hierarchically
Currently the Package Dependencies view lists all dependencies in a flat list. This list not only includes those explicitly defined in the project's Package.swift, but also all the dependencies of dependencies. As such its difficult to tell at a glance how a dependency is included. It can also be confusing to see dependencies in the list that you did not explicitly add. Instead, show a top level list that is only the dependencies explicitly defined in Package.swift. Expanding one shows any child dependencies of the dependency. The files in the dependency's folder are still shown as well, just after any child deps.
1 parent d88a5c8 commit f9979d5

File tree

6 files changed

+329
-279
lines changed

6 files changed

+329
-279
lines changed

src/FolderContext.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,14 @@ export class FolderContext implements vscode.Disposable {
128128
await this.swiftPackage.reloadPackageResolved();
129129
}
130130

131+
/** reload workspace-state.json for this folder */
132+
async reloadWorkspaceState() {
133+
await this.swiftPackage.reloadWorkspaceState();
134+
}
135+
131136
/** Load Swift Plugins and store in Package */
132137
async loadSwiftPlugins() {
133-
const plugins = await SwiftPackage.loadPlugins(
134-
this.folder,
135-
this.workspaceContext.toolchain
136-
);
137-
this.swiftPackage.plugins = plugins;
138+
await this.swiftPackage.loadSwiftPlugins(this.workspaceContext.toolchain);
138139
}
139140

140141
/**

src/PackageWatcher.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import * as vscode from "vscode";
1616
import { FolderContext } from "./FolderContext";
1717
import { FolderOperation, WorkspaceContext } from "./WorkspaceContext";
18+
import { BuildFlags } from "./toolchain/BuildFlags";
1819

1920
/**
2021
* Watches for changes to **Package.swift** and **Package.resolved**.
@@ -25,6 +26,7 @@ import { FolderOperation, WorkspaceContext } from "./WorkspaceContext";
2526
export class PackageWatcher {
2627
private packageFileWatcher?: vscode.FileSystemWatcher;
2728
private resolvedFileWatcher?: vscode.FileSystemWatcher;
29+
private workspaceStateFileWatcher?: vscode.FileSystemWatcher;
2830

2931
constructor(
3032
private folderContext: FolderContext,
@@ -38,6 +40,7 @@ export class PackageWatcher {
3840
install() {
3941
this.packageFileWatcher = this.createPackageFileWatcher();
4042
this.resolvedFileWatcher = this.createResolvedFileWatcher();
43+
this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher();
4144
}
4245

4346
/**
@@ -47,6 +50,7 @@ export class PackageWatcher {
4750
dispose() {
4851
this.packageFileWatcher?.dispose();
4952
this.resolvedFileWatcher?.dispose();
53+
this.workspaceStateFileWatcher?.dispose();
5054
}
5155

5256
private createPackageFileWatcher(): vscode.FileSystemWatcher {
@@ -69,6 +73,20 @@ export class PackageWatcher {
6973
return watcher;
7074
}
7175

76+
private createWorkspaceStateFileWatcher(): vscode.FileSystemWatcher {
77+
const uri = vscode.Uri.joinPath(
78+
vscode.Uri.file(
79+
BuildFlags.buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true)
80+
),
81+
"workspace-state.json"
82+
);
83+
const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath);
84+
watcher.onDidCreate(async () => await this.handleWorkspaceStateChange());
85+
watcher.onDidChange(async () => await this.handleWorkspaceStateChange());
86+
watcher.onDidDelete(async () => await this.handleWorkspaceStateChange());
87+
return watcher;
88+
}
89+
7290
/**
7391
* Handles a create or change event for **Package.swift**.
7492
*
@@ -95,4 +113,14 @@ export class PackageWatcher {
95113
this.workspaceContext.fireEvent(this.folderContext, FolderOperation.resolvedUpdated);
96114
}
97115
}
116+
117+
/**
118+
* Handles a create or change event for **.build/workspace-state.json**.
119+
*
120+
* This will resolve any changes in the workspace-state.
121+
*/
122+
private async handleWorkspaceStateChange() {
123+
await this.folderContext.reloadWorkspaceState();
124+
this.workspaceContext.fireEvent(this.folderContext, FolderOperation.workspaceStateUpdated);
125+
}
98126
}

src/SwiftPackage.ts

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,18 @@ export interface Target {
4747
/** Swift Package Manager dependency */
4848
export interface Dependency {
4949
identity: string;
50-
type?: string; // fileSystem, sourceControl or registry
50+
type?: string;
5151
requirement?: object;
5252
url?: string;
5353
path?: string;
54+
dependencies: Dependency[];
55+
}
56+
57+
export interface ResolvedDependency extends Omit<Omit<Dependency, "type">, "path"> {
58+
version: string;
59+
type: string;
60+
path: string;
61+
location: string;
5462
}
5563

5664
/** Swift Package.resolved file */
@@ -187,18 +195,23 @@ export class SwiftPackage implements PackageContents {
187195
private constructor(
188196
readonly folder: vscode.Uri,
189197
private contents: SwiftPackageState,
190-
public resolved: PackageResolved | undefined
198+
public resolved: PackageResolved | undefined,
199+
private workspaceState: WorkspaceState | undefined
191200
) {}
192201

193202
/**
194203
* Create a SwiftPackage from a folder
195204
* @param folder folder package is in
196205
* @returns new SwiftPackage
197206
*/
198-
static async create(folder: vscode.Uri, toolchain: SwiftToolchain): Promise<SwiftPackage> {
207+
public static async create(
208+
folder: vscode.Uri,
209+
toolchain: SwiftToolchain
210+
): Promise<SwiftPackage> {
199211
const contents = await SwiftPackage.loadPackage(folder, toolchain);
200212
const resolved = await SwiftPackage.loadPackageResolved(folder);
201-
return new SwiftPackage(folder, contents, resolved);
213+
const workspaceState = await SwiftPackage.loadWorkspaceState(folder);
214+
return new SwiftPackage(folder, contents, resolved, workspaceState);
202215
}
203216

204217
/**
@@ -211,15 +224,28 @@ export class SwiftPackage implements PackageContents {
211224
toolchain: SwiftToolchain
212225
): Promise<SwiftPackageState> {
213226
try {
214-
let { stdout } = await execSwift(["package", "describe", "--type", "json"], toolchain, {
227+
// Use swift package describe to describe the package targets, products, and platforms
228+
const describe = await execSwift(["package", "describe", "--type", "json"], toolchain, {
215229
cwd: folder.fsPath,
216230
});
217-
// remove lines from `swift package describe` until we find a "{"
218-
while (!stdout.startsWith("{")) {
219-
const firstNewLine = stdout.indexOf("\n");
220-
stdout = stdout.slice(firstNewLine + 1);
221-
}
222-
return JSON.parse(stdout);
231+
const packageState = JSON.parse(
232+
SwiftPackage.trimStdout(describe.stdout)
233+
) as PackageContents;
234+
235+
// Use swift package show-dependencies to get the dependencies in a tree format
236+
const dependencies = await execSwift(
237+
["package", "show-dependencies", "--format", "json"],
238+
toolchain,
239+
{
240+
cwd: folder.fsPath,
241+
}
242+
);
243+
244+
packageState.dependencies = JSON.parse(
245+
SwiftPackage.trimStdout(dependencies.stdout)
246+
).dependencies;
247+
248+
return packageState;
223249
} catch (error) {
224250
const execError = error as { stderr: string };
225251
// if caught error and it begins with "error: root manifest" then there is no Package.swift
@@ -237,7 +263,9 @@ export class SwiftPackage implements PackageContents {
237263
}
238264
}
239265

240-
static async loadPackageResolved(folder: vscode.Uri): Promise<PackageResolved | undefined> {
266+
private static async loadPackageResolved(
267+
folder: vscode.Uri
268+
): Promise<PackageResolved | undefined> {
241269
try {
242270
const uri = vscode.Uri.joinPath(folder, "Package.resolved");
243271
const contents = await fs.readFile(uri.fsPath, "utf8");
@@ -248,7 +276,7 @@ export class SwiftPackage implements PackageContents {
248276
}
249277
}
250278

251-
static async loadPlugins(
279+
private static async loadPlugins(
252280
folder: vscode.Uri,
253281
toolchain: SwiftToolchain
254282
): Promise<PackagePlugin[]> {
@@ -280,12 +308,12 @@ export class SwiftPackage implements PackageContents {
280308
* Load workspace-state.json file for swift package
281309
* @returns Workspace state
282310
*/
283-
public async loadWorkspaceState(): Promise<WorkspaceState | undefined> {
311+
private static async loadWorkspaceState(
312+
folder: vscode.Uri
313+
): Promise<WorkspaceState | undefined> {
284314
try {
285315
const uri = vscode.Uri.joinPath(
286-
vscode.Uri.file(
287-
BuildFlags.buildDirectoryFromWorkspacePath(this.folder.fsPath, true)
288-
),
316+
vscode.Uri.file(BuildFlags.buildDirectoryFromWorkspacePath(folder.fsPath, true)),
289317
"workspace-state.json"
290318
);
291319
const contents = await fs.readFile(uri.fsPath, "utf8");
@@ -306,6 +334,14 @@ export class SwiftPackage implements PackageContents {
306334
this.resolved = await SwiftPackage.loadPackageResolved(this.folder);
307335
}
308336

337+
public async reloadWorkspaceState() {
338+
this.workspaceState = await SwiftPackage.loadWorkspaceState(this.folder);
339+
}
340+
341+
public async loadSwiftPlugins(toolchain: SwiftToolchain) {
342+
this.plugins = await SwiftPackage.loadPlugins(this.folder, toolchain);
343+
}
344+
309345
/** Return if has valid contents */
310346
public get isValid(): boolean {
311347
return isPackage(this.contents);
@@ -325,6 +361,88 @@ export class SwiftPackage implements PackageContents {
325361
return this.contents !== undefined;
326362
}
327363

364+
public rootDependencies(): ResolvedDependency[] {
365+
// Correlate the root dependencies found in the Package.swift with their
366+
// checked out versions in the workspace-state.json.
367+
const result = this.dependencies.map(dependency =>
368+
this.resolveDependencyAgainstWorkspaceState(dependency)
369+
);
370+
return result;
371+
}
372+
373+
private resolveDependencyAgainstWorkspaceState(dependency: Dependency): ResolvedDependency {
374+
const workspaceStateDep = this.workspaceState?.object.dependencies.find(
375+
dep => dep.packageRef.identity === dependency.identity
376+
);
377+
return {
378+
...dependency,
379+
version: workspaceStateDep?.state.checkoutState?.version ?? "",
380+
path: workspaceStateDep
381+
? this.dependencyPackagePath(workspaceStateDep, this.folder.fsPath)
382+
: "",
383+
type: workspaceStateDep ? this.dependencyType(workspaceStateDep) : "",
384+
location: workspaceStateDep ? workspaceStateDep.packageRef.location : "",
385+
};
386+
}
387+
388+
public async childDependencies(dependency: Dependency): Promise<ResolvedDependency[]> {
389+
return dependency.dependencies.map(dep => this.resolveDependencyAgainstWorkspaceState(dep));
390+
}
391+
392+
/**
393+
* * Get package source path of dependency
394+
* `editing`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath
395+
* `local`: dependency.packageRef.location
396+
* `remote`: buildDirectory + checkouts + dependency.packageRef.location
397+
* @param dependency
398+
* @param workspaceFolder
399+
* @return the package path based on the type
400+
*/
401+
private dependencyPackagePath(
402+
dependency: WorkspaceStateDependency,
403+
workspaceFolder: string
404+
): string {
405+
const type = this.dependencyType(dependency);
406+
if (type === "editing") {
407+
return (
408+
dependency.state.path ?? path.join(workspaceFolder, "Packages", dependency.subpath)
409+
);
410+
} else if (type === "local") {
411+
return dependency.state.path ?? dependency.packageRef.location;
412+
} else {
413+
// remote
414+
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(
415+
workspaceFolder,
416+
true
417+
);
418+
if (dependency.packageRef.kind === "registry") {
419+
return path.join(buildDirectory, "registry", "downloads", dependency.subpath);
420+
} else {
421+
return path.join(buildDirectory, "checkouts", dependency.subpath);
422+
}
423+
}
424+
}
425+
426+
/**
427+
* Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local
428+
* @param dependency
429+
* @return "local" | "remote" | "editing"
430+
*/
431+
private dependencyType(dependency: WorkspaceStateDependency): "local" | "remote" | "editing" {
432+
if (dependency.state.name === "edited") {
433+
return "editing";
434+
} else if (
435+
dependency.packageRef.kind === "local" ||
436+
dependency.packageRef.kind === "fileSystem"
437+
) {
438+
// need to check for both "local" and "fileSystem" as swift 5.5 and earlier
439+
// use "local" while 5.6 and later use "fileSystem"
440+
return "local";
441+
} else {
442+
return "remote";
443+
}
444+
}
445+
328446
/** name of Swift Package */
329447
get name(): string {
330448
return (this.contents as PackageContents)?.name ?? "";
@@ -375,6 +493,15 @@ export class SwiftPackage implements PackageContents {
375493
const filePath = path.relative(this.folder.fsPath, file);
376494
return this.targets.find(target => isPathInsidePath(filePath, target.path));
377495
}
496+
497+
private static trimStdout(stdout: string): string {
498+
// remove lines from `swift package describe` until we find a "{"
499+
while (!stdout.startsWith("{")) {
500+
const firstNewLine = stdout.indexOf("\n");
501+
stdout = stdout.slice(firstNewLine + 1);
502+
}
503+
return stdout;
504+
}
378505
}
379506

380507
export enum TargetType {

src/WorkspaceContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,8 @@ export enum FolderOperation {
680680
packageUpdated = "packageUpdated",
681681
// Package.resolved has been updated
682682
resolvedUpdated = "resolvedUpdated",
683+
// .build/workspace-state.json has been updated
684+
workspaceStateUpdated = "workspaceStateUpdated",
683685
}
684686

685687
/** Workspace Folder Event */

0 commit comments

Comments
 (0)