Skip to content

Commit

Permalink
feat: enhance graph api with legacy methods directly attached to skot…
Browse files Browse the repository at this point in the history
…t instance (#64)

* refactor: move graph api previously tight to skott

* docs: add api documentation corresponding to the latest api shape

* feat: add getFileNode method

* cleanup

* docs: add changeset
  • Loading branch information
antoine-coulon committed Jul 4, 2023
1 parent 3b0342e commit 10fac91
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-peaches-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"skott": minor
---

Breaking Changes: move `findCircularDependencies`, `hasCircularDependencies`, `findLeaves` inside `useGraph` api encapsulation.
121 changes: 92 additions & 29 deletions packages/skott/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## How to use skott

### Install
## Install

You can install skott either locally or globally
```bash
Expand All @@ -13,7 +13,7 @@ npm install skott
npm install skott -g
```

### **Embedded Web Application**
## **Embedded Web Application**

skott now embeds a new _display mode_ **"skott --displayMode=webapp"** allowing you to visualize more precisely dependencies and the links between them. Here is an overview of a subset from the graph generated for `fastify`:

Expand All @@ -26,12 +26,12 @@ When `Circular dependencies` are found in the graph, they can also be toggled vi
<img alt="skott-webapp-with-cycles" src="https://user-images.githubusercontent.com/43391199/204466577-3b82bf6c-4ed4-436c-bd99-31aa9261fb61.png" />


### **JavaScript API**
## **JavaScript API**

```javascript
import skott from "skott";

const { getStructure, getWorkspace, findCircularDependencies, findParentsOf, findLeaves } = await skott({
const { getStructure, getWorkspace, useGraph, findUnusedDependencies } = await skott({
/**
* (Optional) Entrypoint of the project. If not provided, `skott` will search for all
* supported files starting from the current working directory.
Expand All @@ -44,7 +44,7 @@ const { getStructure, getWorkspace, findCircularDependencies, findParentsOf, fin
* graph.
* Defaults to `none`;
*/
ignorePattern: "src/examples/**/*"
ignorePattern: "src/examples/**/*",
/**
* (Optional) Whether to run Skott using the incremental pattern. By setting "true",
* Skott will create a `.skott/cache.json` file to only detect and re-process what
Expand Down Expand Up @@ -89,7 +89,7 @@ const { getStructure, getWorkspace, findCircularDependencies, findParentsOf, fin
thirdParty: true,
builtin: true,
typeOnly: true
};
},
/**
* (Optional) Provide a custom tsconfig file to help skott resolve path aliases.
* When extending some other tsconfig files, skott will be able to parse
Expand Down Expand Up @@ -118,7 +118,7 @@ const { getStructure, getWorkspace, findCircularDependencies, findParentsOf, fin
});
```

### **Command line interface**
## **Command line interface**

skott exposes a CLI directly using features from the core library.

Expand Down Expand Up @@ -187,7 +187,7 @@ See all the options of the CLI running:
$ skott --help
```

## Examples
## API Documentation

To initialize the dependency graph, the default exported function must be used first.
Once executed, the default function returns a set of functions to retrieve some
Expand All @@ -206,34 +206,68 @@ console.log(graph); // logs { "index.js": { id: "index.js", adjacentTo: [], bod
console.log(files); // logs [ "index.js" ]
```

### Search for circular dependencies
### Graph API

To easily consume the graph that was emitted while exploring the project, skott exposes a graph API including various methods to traverse all the nodes, collect parent and children dependencies, find circular dependencies, and more.

```javascript
import skott from "skott";

const { findCircularDependencies, hasCircularDependencies } = await skott({
entrypoint: "index.js",
// ...rest of the config
});
const { useGraph } = await skott();

const {
getFileNode,
traverseFiles,
collectFilesDependencies,
collectFilesDependingOn,
findLeaves,
findCircularDependencies,
hasCircularDependencies 
} = useGraph();
```

// Imagine that starting from "index.js" skott detects a circular dependency
// between "core.js" and "utils.js" files
### Graph walking

console.log(findCircularDependencies()); // logs [ [ "core.js", "utils.js" ] ]
console.log(hasCircularDependencies()); // logs "true"
```javascript
const { useGraph } = await skott();
const { traverseFiles } = useGraph();

// Starting from any node, walking the whole graph
for(const file of traverseFiles()) {
// SkottNode { }
}

// Starting from a specifc node, walking the graph from it
for(const file of traverseFiles({ rootFile: "index.js" })) {
// SkottNode { }
}

// By default, skott will collect "shallow first" files in a Breadth-First fashion
// meaning the iterator will first emit direct module imports for each visited node.
// If the traversal needs to be "deep first" instead i.e. you first want to go deep
// down through the graph until meeting a leaf you might want to use "deepFirst" option
// to turn the traversal into Depth-First search.

for(const file of traverseFiles({ rootFile: "index.js", traversal: "deepFirst" })) {
// SkottNode { }
}
```

### Search for unused dependencies using the graph generated
### Search for circular dependencies

```javascript
import skott from "skott";

const { findUnusedDependencies } = await skott({
entrypoint: "index.tsx",
// ...rest of the config
const { useGraph } = await skott({
entrypoint: "index.js",
});
const { findCircularDependencies, hasCircularDependencies} = useGraph();

// Imagine that starting from "index.js" skott detects a circular dependency
// between "core.js" and "utils.js" files

const { thirdParty } = await findUnusedDependencies();
console.log(thirdParty); // logs [ "rxjs", "lodash.difference" ]
console.log(findCircularDependencies()); // logs [ [ "core.js", "utils.js" ] ]
console.log(hasCircularDependencies()); // logs "true"
```

### Search for leaves (nodes with no children)
Expand All @@ -249,15 +283,15 @@ index.js
```javascript
import skott from "skott";

const { findLeaves } = await skott({
const { useGraph } = await skott({
entrypoint: "leaf.js",
// ...rest of the config
});
const { findLeaves } = useGraph();

console.log(findLeaves()); // logs [ "leaf.js" ]
```

### Deeply search for parent dependencies of a given node
### Deeply or Shallowly search for parent or children dependencies of a given node

children.js

Expand All @@ -277,13 +311,42 @@ index.js

```javascript
import skott from "skott";
import { CollectLevel } from "skott/graph/traversal";

const { findParentsOf } = await skott({
const { useGraph } = await skott({
entrypoint: "parent.js",
// ...rest of the config
});
const { collectFilesDependingOn, collectFilesDependencies } = useGraph();

// CollectLevel.Deep or CollectLevel.Shallow. In that case just one level so we can use Shallow

console.log(collectFilesDependingOn("children.js", CollectLevel.Shallow));
// logs [ SkottNode { id: "parent.js" } ]

console.log(collectFilesDependencies("parent.js", CollectLevel.Shallow));
// logs [ SkottNode { id: "children.js" } ]
```

### Find unused dependencies

skott provides a way to walk through dependencies listed in the current working directory manifest (package.json) and compare them to what it founds and marked as "used" during the analysis. The "use" marking will be done when a third-party module appears to be imported in the source code that was walked. All the third-party dependencies that are not used in the traversed files will be returned as "unused".

Additionnally to the source code analysis, skott integrates with [depcheck](https://github.com/depcheck/depcheck) allowing it to take a peak at "implicit" dependencies and emit hypothesis about whether some `devDependencies` are unused or not, by walking through most common config files.

Note: finding precisely implicit dependencies is hard so please double check dependencies part of the `devDependencies` that are marked as "unused" by the analysis. If some `dependencies` (production deps) appear to be unused but are indeed used somewhere in the codebase, it could mean two things:

- the input files pattern you provided to skott don't cover the parts of the graph where the dependency is used
- the dependency is used nowhere through the source code files walked, meaning that it should probably be moved to `devDependencies` or just get removed.

In any case, `unused dependencies` just raise an alert so I would advise to double check before getting rid of a dependency.

```javascript
import skott from "skott";

const { findUnusedDependencies } = await skott();

console.log(findParentsOf("children.js")); // logs [ "parent.js" ]
constthirdParty } = await findUnusedDependencies();
// [ lodash, rxjs, typescript ]
```

### Explore file node metadata
Expand Down
3 changes: 2 additions & 1 deletion packages/skott/bin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ function makeCircularDependenciesUI(
options: CliOptions
): string[][] {
const circularDependencies: string[][] = [];
const { findCircularDependencies, hasCircularDependencies } = skottInstance;
const { findCircularDependencies, hasCircularDependencies } =
skottInstance.useGraph();

// only find circular dependencies on-demand as it can be expensive
if (options.showCircularDependencies) {
Expand Down
2 changes: 1 addition & 1 deletion packages/skott/bin/ui/webapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function openWebApplication(
console.log(`\n ${kleur.italic("Prefetching data...")} `);

srv.get("/api/cycles", (_, response: ServerResponse) => {
const cycles = skottInstance.findCircularDependencies();
const cycles = skottInstance.useGraph().findCircularDependencies();

response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(cycles));
Expand Down
28 changes: 27 additions & 1 deletion packages/skott/src/graph/traversal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DiGraph } from "digraph-js";
import type { SkottNode } from "./node.js";
import { SkottConfig } from "../skott.js";

export const CollectLevel = {
Deep: "deep",
Expand All @@ -10,6 +11,7 @@ export type CollectLevelValues =
(typeof CollectLevel)[keyof typeof CollectLevel];

export interface TraversalApi<T> {
getFileNode(id: string): SkottNode<T>;
traverseFiles: (options?: {
rootFile?: string;
moduleImportsCollection?: "deepFirst" | "shallowFirst";
Expand All @@ -22,6 +24,9 @@ export interface TraversalApi<T> {
rootFile: string,
level: CollectLevelValues
) => SkottNode<T>[];
findLeaves: () => string[];
findCircularDependencies: () => string[][];
hasCircularDependencies: () => boolean;
}

const skottToDiGraphTraversal = {
Expand All @@ -30,11 +35,14 @@ const skottToDiGraphTraversal = {
} as const;

export function makeTraversalApi<T>(
graph: DiGraph<SkottNode<T>>
graph: DiGraph<SkottNode<T>>,
config: SkottConfig<T>
): TraversalApi<T> {
const nodes = graph.toDict();

return {
getFileNode: (id) => nodes[id],

*traverseFiles(options) {
const rootNode = options?.rootFile;
const moduleImportsCollection =
Expand Down Expand Up @@ -68,6 +76,24 @@ export function makeTraversalApi<T>(
}

return Array.from(graph.getDeepParents(rootFile)).map((id) => nodes[id]);
},

hasCircularDependencies(): boolean {
return graph.hasCycles({
maxDepth: config.circularMaxDepth ?? Number.POSITIVE_INFINITY
});
},

findCircularDependencies(): SkottNode["id"][][] {
return graph.findCycles({
maxDepth: config.circularMaxDepth ?? Number.POSITIVE_INFINITY
});
},

findLeaves(): SkottNode["id"][] {
return Object.entries(nodes)
.filter(([_, node]) => node.adjacentTo.length === 0)
.map(([leafId]) => leafId);
}
};
}
36 changes: 1 addition & 35 deletions packages/skott/src/skott.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,9 @@ export interface SkottInstance<T = unknown> {
useGraph: () => TraversalApi<T>;
getStructure: () => SkottStructure<T>;
getWorkspace: () => ManifestDependenciesByName;
findLeaves: () => string[];
findCircularDependencies: () => string[][];
findUnusedDependencies: (
options?: ImplicitUnusedDependenciesOptions
) => Promise<UnusedDependencies>;
hasCircularDependencies: () => boolean;
findParentsOf: (node: string) => string[];
}

export const defaultConfig = {
Expand Down Expand Up @@ -405,32 +401,6 @@ export class Skott<T> {
}
}

private hasCircularDependencies(): boolean {
return this.#projectGraph.hasCycles({
maxDepth: this.config.circularMaxDepth ?? Number.POSITIVE_INFINITY
});
}

private circularDependencies(): string[][] {
return this.#projectGraph.findCycles({
maxDepth: this.config.circularMaxDepth ?? Number.POSITIVE_INFINITY
});
}

private findLeaves(): string[] {
return Object.entries(this.#projectGraph.toDict())
.filter(([_, node]) => node.adjacentTo.length === 0)
.map(([leafId]) => leafId);
}

private findParentsOf(node: string): string[] {
const uniqueSetOfParents = new Set<string>([
...this.#projectGraph.getDeepParents(node)
]);

return [...uniqueSetOfParents];
}

private findThirdPartyDependenciesFromGraph(): string[] {
const graphDependencies = new Set<string>();

Expand Down Expand Up @@ -599,13 +569,9 @@ export class Skott<T> {
}

return {
useGraph: () => makeTraversalApi(this.#projectGraph),
useGraph: () => makeTraversalApi(this.#projectGraph, this.config),
getStructure: this.makeProjectStructure.bind(this),
getWorkspace: () => this.#workspaceConfiguration.manifests,
findCircularDependencies: this.circularDependencies.bind(this),
hasCircularDependencies: this.hasCircularDependencies.bind(this),
findLeaves: this.findLeaves.bind(this),
findParentsOf: this.findParentsOf.bind(this),
findUnusedDependencies: this.findUnusedDependencies.bind(this)
};
}
Expand Down
Loading

0 comments on commit 10fac91

Please sign in to comment.