Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ Specifically, LayerLens:
1. Restricts each diagram to the content of a single directory,
so that every directory has its own diagram.

3. For each directory treats immediate sub-directories and files as equal elements,
1. For each directory treats immediate sub-directories and files as equal elements,
and shows dependencies between these elements as a directed graph.

As result:

1. Each directory diagram is simple. It does not contain (1) any internal details of directories or files, and (2)
any details of code outside the directory.

2. All diagrams together are enough to detect cycles in application.
1. All diagrams together are enough to detect cycles in application.

<img width="536" alt="Screenshot 2023-01-14 at 9 45 33 PM" src="https://user-images.githubusercontent.com/12115586/212524921-5221785f-692d-4464-a230-0f620434e2c5.png">
<img width="536" alt="Example output" src="./screenshots/DEPENDENCIES.png">

## Configure layerlens

Expand Down Expand Up @@ -85,8 +85,8 @@ To see the diagrams in your IDE:

Make your pre-submit bots failing in case of issues, using flags:

* `--fail-on-cycles`: fail if there are dependency cycles
* `--fail-if-changed`: fail if the generated diagrams has changed
- `--fail-on-cycles`: fail if there are dependency cycles
- `--fail-if-changed`: fail if the generated diagrams has changed

### Re-generate on every GitHub push

Expand All @@ -102,11 +102,21 @@ formatted as [glob](https://pub.dev/packages/glob) syntax.

For example, to generate the diagrams:

* only for the root `lib/` folder: `dart run layerlens --only "lib"`
* only for the root `lib/` folder: `dart run layerlens --only "lib"`
* for all folders except `l10n/`: `dart run layerlens --except "l10n"`
* only for root `lib/` and it's subfolder: run `layerlens --only "lib" --only "lib/subfolder1"`
* for the entire subtree for a given subfolder: `layerlens --only "lib/subfolder1" --only "lib/subfolder1/**"`
- only for the root `lib/` folder: `dart run layerlens --only "lib"`
- only for the root `lib/` folder: `dart run layerlens --only "lib"`
- for all folders except `l10n/`: `dart run layerlens --except "l10n"`
- only for root `lib/` and it's subfolder: run `layerlens --only "lib" --only "lib/subfolder1"`
- for the entire subtree for a given subfolder: `layerlens --only "lib/subfolder1" --only "lib/subfolder1/**"`

## Alternative layout engines

For more complex dependency graphs, you may want to use the [`elk` layout engine](https://mermaid.js.org/syntax/flowchart.html#renderer) in your Mermaid diagrams:

|`--layout=dagre` (default)|`--layout=elk`|
|:---:|:---:|
|![Dagre layout engine](./screenshots/DEFAULT%20LAYOUT.png)|![Elk Layout Engine](./screenshots/ELK%20LAYOUT.png)|

>*Note: Some VSCode Markdown plugins do not support the `elk` engine yet, and will not produce different outputs to the default!.*

## Supported languages

Expand Down
24 changes: 24 additions & 0 deletions bin/layerlens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ import 'package:args/args.dart';
import 'package:glob/glob.dart';
import 'package:layerlens/layerlens.dart';
import 'package:layerlens/src/cli.dart';
import 'package:layerlens/src/generator.dart';
import 'package:layerlens/src/model.dart';

const _filterDocLink =
'https://github.com/polina-c/layerlens/blob/main/README.md#filter';

const _mermaidLayoutDocLink =
'https://mermaid.js.org/syntax/flowchart.html#renderer';

void main(List<String> args) async {
final parser = ArgParser()
..addFlag(
Expand Down Expand Up @@ -66,6 +70,18 @@ void main(List<String> args) async {
CliOptions.except.name,
help: 'Which folders to exclude from diagram generation. Use glob syntax.'
'\n$_filterDocLink',
)
..addOption(
CliOptions.layout.name,
help: 'Layout engine to use for Mermaid diagrams.'
'\nCertain markdown preview plugins may not support all layout engines.'
'\n$_mermaidLayoutDocLink.',
defaultsTo: 'dagre',
allowed: ['elk', 'dagre'],
allowedHelp: {
'dagre': 'Mermaid\'s default layout engine.',
'elk': 'ELK layout engine (experimental).',
},
);

late final ArgResults parsedArgs;
Expand Down Expand Up @@ -96,12 +112,20 @@ void main(List<String> args) async {
);
}

// Prepare Mermaid options, extendable.
List<MermaidOption> mermaidOptions() {
return [
(key: 'layout', value: parsedArgs[CliOptions.layout.name]),
];
}

final generatedDiagrams = await generateLayering(
rootDir: parsedArgs[CliOptions.path.name],
packageName: parsedArgs[CliOptions.package.name],
failOnCycles: parsedArgs[CliOptions.failOnCycles.name] as bool,
failIfChanged: parsedArgs[CliOptions.failIfChanged.name] as bool,
filter: filter(),
mermaidOptions: mermaidOptions(),
);
print(
'Generated $generatedDiagrams diagrams. Check files DEPS.md in source folders.',
Expand Down
2 changes: 2 additions & 0 deletions lib/layerlens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Future<int> generateLayering({
required bool failOnCycles,
required bool failIfChanged,
required Filter filter,
required List<MermaidOption> mermaidOptions,
ExitCallback exitFn = exit,
}) async {
final deps = await collectDeps(
Expand All @@ -43,5 +44,6 @@ Future<int> generateLayering({
filter: filter,
failIfChanged: failIfChanged,
failOnCycles: failOnCycles,
mermaidOptions: mermaidOptions,
).generateFiles();
}
2 changes: 1 addition & 1 deletion lib/src/analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Analyzer {

/// Recursively adds file, and folders for the file path, to the
/// source tree.
_propagateFileToFolder(
void _propagateFileToFolder(
SourceFolder folder,
SourceFile file,
List<String> path,
Expand Down
1 change: 1 addition & 0 deletions lib/src/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum CliOptions {
help('help'),
failOnCycles('fail-on-cycles'),
failIfChanged('fail-if-changed'),
layout('layout'),
;

const CliOptions(this.name);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/code_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class _DepsCollector extends RecursiveAstVisitor
return super.visitExportDirective(node);
}

_collectDep(Dependency? dependency) {
void _collectDep(Dependency? dependency) {
if (dependency == null) return;

if (!collectedDeps.containsKey(dependency.consumer)) {
Expand Down
29 changes: 27 additions & 2 deletions lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,32 @@ import 'package:path/path.dart' as path;
import 'cli.dart';
import 'model.dart';

typedef MermaidOption = ({String key, String value});

class MdGenerator {
final String rootDir;
final SourceFolder sourceFolder;
final Filter filter;
final bool failIfChanged;
final bool failOnCycles;
final ExitCallback? exitFn;
final List<MermaidOption> mermaidOptions;

MdGenerator({
required this.rootDir,
required this.sourceFolder,
required this.filter,
required this.failIfChanged,
required this.failOnCycles,
required this.mermaidOptions,
this.exitFn,
});

/// Default Mermaid options, filtered before content is generated.
static const List<MermaidOption> _defaultMermaidOptions = [
(key: 'layout', value: 'dagre'),
];

Future<int> generateFiles() async {
return await _generateRecursive(sourceFolder);
}
Expand Down Expand Up @@ -69,7 +78,7 @@ class MdGenerator {
return false;
}

final diagram = content(folder);
final diagram = content(folder, mermaidOptions);
if (diagram == null) {
await deleteDiagramFile(path: filePath, failIfExists: failIfChanged);
return false;
Expand All @@ -86,7 +95,10 @@ class MdGenerator {
return true;
}

static String? content(SourceFolder folder) {
static String? content(
SourceFolder folder,
List<MermaidOption> mermaidOptions,
) {
final items = <String>[];
for (final consumer in folder.children.values) {
for (final dependency in consumer.siblingDependencies) {
Expand All @@ -100,6 +112,9 @@ class MdGenerator {

items.sort();
final result = StringBuffer();
final filteredOptions = mermaidOptions
.where((option) => !_defaultMermaidOptions.contains(option))
.toList();

result.writeln('<!---');
result.writeln('Generated by https://github.com/polina-c/layerlens');
Expand All @@ -109,6 +124,16 @@ class MdGenerator {
result.writeln('-->');
result.writeln('');
result.writeln('```mermaid');

if (filteredOptions.isNotEmpty) {
result.writeln('---');
result.writeln('config:');
for (final option in filteredOptions) {
result.writeln(' ${option.key}: ${option.value}');
}
result.writeln('---');
}

result.writeln('flowchart TD;');
result.writeln(items.join('\n'));
result.writeln('```');
Expand Down
18 changes: 3 additions & 15 deletions lib/src/layering.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ void assignLayers(SourceFolder folder) {
folder.totalInversions = folder.localInversions + inversionsInSubfolders;
}

// TODO(polina-c): convert to record.
class _NodesAndValue {
final Set<SourceNode> nodes;
final int value;

_NodesAndValue(this.nodes, this.value);
}
typedef _NodesAndValue = ({Set<SourceNode> nodes, int value});

void _assignLocalLayerToChildren(SourceFolder folder) {
// Items without layer.
Expand Down Expand Up @@ -98,10 +92,7 @@ _NodesAndValue _withMinConsumersWl(Set<SourceNode> nodes) {

final minNumber = byNumberOfConsumersWl.keys.reduce((v, e) => min(v, e));

return _NodesAndValue(
byNumberOfConsumersWl[minNumber]!,
minNumber,
);
return (nodes: byNumberOfConsumersWl[minNumber]!, value: minNumber);
}

_NodesAndValue _withMaxDependenciesWl(Set<SourceNode> nodes) {
Expand All @@ -124,8 +115,5 @@ _NodesAndValue _withMaxDependenciesWl(Set<SourceNode> nodes) {

final maxNumber = byNumberOfDependenciesWl.keys.reduce((v, e) => max(v, e));

return _NodesAndValue(
byNumberOfDependenciesWl[maxNumber]!,
maxNumber,
);
return (nodes: byNumberOfDependenciesWl[maxNumber]!, value: maxNumber);
}
2 changes: 1 addition & 1 deletion lib/src/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class SourceFolder extends SourceNode {
}

class SourceFile extends SourceNode {
SourceFile(FullName fullName) : super.parse(fullName);
SourceFile(super.fullName) : super.parse();

final Set<SourceFile> dependencies = {};
}
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: layerlens
version: 3.0.4
version: 3.1.0
description: Generate a dependency diagram in every folder of your source code.
repository: https://github.com/polina-c/layerlens

Expand All @@ -20,5 +20,5 @@ dependencies:

dev_dependencies:
file: ^7.0.0
lints: ^2.0.0
lints: ^6.0.0
test: ^1.21.0
Binary file added screenshots/DEFAULT LAYOUT.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added screenshots/ELK LAYOUT.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading