From 59908d88b17f9627f24ab4246b71541548978bc3 Mon Sep 17 00:00:00 2001 From: Ammar Najjar Date: Sat, 24 Dec 2022 23:19:23 +0100 Subject: [PATCH 1/3] fix chaining nested actions --- .eslintignore | 3 +- .eslintrc | 3 +- README.md | 167 ++++++++++++++++++-- docs/example.dot | 20 --- docs/example.svg | 150 ------------------ docs/examples/case1/action1.dot | 9 ++ docs/examples/case1/action1.svg | 66 ++++++++ docs/examples/case1/action3.dot | 9 ++ docs/examples/case1/action3.svg | 66 ++++++++ docs/examples/case1/ngrx-graph.json | 15 ++ docs/examples/case2/action1.dot | 14 ++ docs/examples/case2/action1.svg | 90 +++++++++++ docs/examples/case2/action3.dot | 12 ++ docs/examples/case2/action3.svg | 78 ++++++++++ docs/examples/case2/ngrx-graph.json | 15 ++ docs/keys/action.png | Bin 0 -> 9422 bytes docs/keys/component.png | Bin 0 -> 10498 bytes docs/keys/nestedAction.png | Bin 0 -> 22034 bytes docs/keys/reducer.png | Bin 0 -> 13413 bytes docs/keys/selectedAction.png | Bin 0 -> 9900 bytes src/commands/graph/index.ts | 2 +- src/generator.ts | 232 ++++++++++++++++++++++------ 22 files changed, 723 insertions(+), 228 deletions(-) delete mode 100644 docs/example.dot delete mode 100644 docs/example.svg create mode 100644 docs/examples/case1/action1.dot create mode 100644 docs/examples/case1/action1.svg create mode 100644 docs/examples/case1/action3.dot create mode 100644 docs/examples/case1/action3.svg create mode 100644 docs/examples/case1/ngrx-graph.json create mode 100644 docs/examples/case2/action1.dot create mode 100644 docs/examples/case2/action1.svg create mode 100644 docs/examples/case2/action3.dot create mode 100644 docs/examples/case2/action3.svg create mode 100644 docs/examples/case2/ngrx-graph.json create mode 100644 docs/keys/action.png create mode 100644 docs/keys/component.png create mode 100644 docs/keys/nestedAction.png create mode 100644 docs/keys/reducer.png create mode 100644 docs/keys/selectedAction.png diff --git a/.eslintignore b/.eslintignore index 9b1c8b1..87dc048 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -/dist +dist +assets diff --git a/.eslintrc b/.eslintrc index 33e8585..1d53b7b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,6 +5,7 @@ "semi": "off", "object-curly-spacing": "off", "unicorn/no-array-reduce": "off", - "unicorn/prefer-object-from-entries": "off" + "unicorn/prefer-object-from-entries": "off", + "operator-linebreak": "off" } } diff --git a/README.md b/README.md index 5687857..d38a1d7 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,167 @@ for file in *.dot; do; dot -Tsvg $file -o "${file%.*}".svg; rm $file; done The first run will generate a json file (see `--structureFile` flag), which is used for the next runs if the flag `--force` was not set as cache. If this file exists, source code will not be parsed for actions, the recorded structure will be taken from that json file. This speeds up the process considerably. -## Example +
+ Graph Keys + +| | | +| --------------- | -------------------------------------------- | +| Component | ![component](./docs/keys/component.png) | +| Action | ![component](./docs/keys/action.png) | +| Action in focus | ![component](./docs/keys/selectedAction.png) | +| Nested Action | ![component](./docs/keys/nestedAction.png) | +| Reducer | ![component](./docs/keys/reducer.png) | + +
+
+ Examples + +### Case 1: + +### Input: + +```typescript +// declarations +export const action1 = createAction('Action1'); +export const action2 = createAction('Action2'); +export const action3 = createAction('Action3'); + +// component +@Component() +export class FirstComponent { + onEvent() { + this.store.dispatch(action1()); + } +} + +// effects +@Injectable() +export class ExampleEffects { + effect1$ = createEffect(() => + this.actions$.pipe( + ofType(action1), + switchMap(() => [action2(), action3()]), + ), + ); +} + +// reducer +const firstReducer = createReducer( + on(action3, () => { + // ... + }), +); +``` + +### Output: + +```bash +npx ngrx-graph -j -f +``` + +- [ngrx-graph.json](./docs/examples/case1/ngrx-graph.json) ```bash -npx ngrx-graph MainAction +npx ngrx-graph action1 +``` + +- [dotFile](./docs/examples/case1/action1.dot) +- graph: + ![graph](./docs/examples/case1/action1.svg) + +```bash +npx ngrx-graph action3 +``` + +- [dotFile](./docs/examples/case1/action3.dot) +- graph: + ![graph](./docs/examples/case1/action3.svg) + +### Case 2 (nested actions): + +### Input: + +```typescript +// declarations +export const nestedAction = createAction( + 'NestedAction', + props<{ action: Action }>(), +); +export const action1 = createAction('Action1'); +export const action2 = createAction('Action2'); +export const action3 = createAction('Action3'); + +// component +@Component() +export class FirstComponent { + onEvent() { + this.store.dispatch(nestedAction({ action: action1() })); + } +} + +// effects +@Injectable() +export class ExampleEffects { + effect1$ = createEffect(() => + this.actions$.pipe( + ofType(action1), + switchMap(() => [nestedAction1({ action: action2() }), action3()])), + ), + ); + + effect2$ = createEffect(() => + this.actions$.pipe( + ofType(nestedAction1), + map(({ action }) => nestedAction2( { action: action()})), + ), + ); + + effect3$ = createEffect(() => + this.actions$.pipe( + ofType(nestedAction2), + map(({ action }) => action())), + ), + ); +} + +// reducer +const firstReducer = createReducer( + on(action3, () => { + // ... + }), +) ``` -Generates: [dot file](.//docs/example.dot) (I took a real world example and anonymised the names) +### Output: + +```bash +npx ngrx-graph -j -f +``` + +- [ngrx-graph.json](./docs/examples/case2/ngrx-graph.json) + +```bash +npx ngrx-graph action1 +``` + +- [dotFile](./docs/examples/case2/action1.dot) +- graph: + ![graph](./docs/examples/case2/action1.svg) + +```bash +npx ngrx-graph action3 +``` -Produced graph will look like: +- [dotFile](./docs/examples/case2/action3.dot) +- graph: + ![graph](./docs/examples/case2/action3.svg) -![example generated graph](./docs/example.svg) +
-# Usage +
+ Usage - + ```sh-session $ npm install -g ngrx-graph @@ -50,10 +196,12 @@ USAGE ``` +
-# Commands +
+ Commands - + - [`ngrx-graph graph [ACTION]`](#ngrx-graph-graph-action) - [`ngrx-graph help [COMMAND]`](#ngrx-graph-help-command) @@ -111,6 +259,7 @@ DESCRIPTION _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.19/src/commands/help.ts)_ +
# Status: diff --git a/docs/example.dot b/docs/example.dot deleted file mode 100644 index 4046760..0000000 --- a/docs/example.dot +++ /dev/null @@ -1,20 +0,0 @@ -digraph { -Component1 [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] - Component1 -> action1 -Component2 [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] -MainAction [color=green, fillcolor=green, fontcolor=white, style=filled] - Component2 -> MainAction -Reducer1 [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] - action5 -> Reducer1 -Reducer2 [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] - action6 -> Reducer2 -Reducer2 [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] - action2 -> Reducer2 -Reducer2 [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] - action1 -> Reducer2 -MainAction -action2 -> action1 -action2 -> MainAction -action3,action5 -> action4 -action3,action5 -> action2 -} diff --git a/docs/example.svg b/docs/example.svg deleted file mode 100644 index 317b12b..0000000 --- a/docs/example.svg +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - -Component1 - -Component1 - - - -action1 - -action1 - - - -Component1->action1 - - - - - -Reducer2 - -Reducer2 - - - -action1->Reducer2 - - - - - -Component2 - -Component2 - - - -MainAction - -MainAction - - - -Component2->MainAction - - - - - -Reducer1 - -Reducer1 - - - -action5 - -action5 - - - -action5->Reducer1 - - - - - -action2 - -action2 - - - -action5->action2 - - - - - -action4 - -action4 - - - -action5->action4 - - - - - -action6 - -action6 - - - -action6->Reducer2 - - - - - -action2->action1 - - - - - -action2->MainAction - - - - - -action2->Reducer2 - - - - - -action3 - -action3 - - - -action3->action2 - - - - - -action3->action4 - - - - - diff --git a/docs/examples/case1/action1.dot b/docs/examples/case1/action1.dot new file mode 100644 index 0000000..e054650 --- /dev/null +++ b/docs/examples/case1/action1.dot @@ -0,0 +1,9 @@ +digraph { +FirstComponent [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] +action1 [color=green, fillcolor=green, fontcolor=white, style=filled] + FirstComponent -> action1 +action1 -> action2 +action1 -> action3 +firstReducer [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] + action3 -> firstReducer +} diff --git a/docs/examples/case1/action1.svg b/docs/examples/case1/action1.svg new file mode 100644 index 0000000..604c693 --- /dev/null +++ b/docs/examples/case1/action1.svg @@ -0,0 +1,66 @@ + + + + + + + + + +FirstComponent + +FirstComponent + + + +action1 + +action1 + + + +FirstComponent->action1 + + + + + +action2 + +action2 + + + +action1->action2 + + + + + +action3 + +action3 + + + +action1->action3 + + + + + +firstReducer + +firstReducer + + + +action3->firstReducer + + + + + diff --git a/docs/examples/case1/action3.dot b/docs/examples/case1/action3.dot new file mode 100644 index 0000000..97058e6 --- /dev/null +++ b/docs/examples/case1/action3.dot @@ -0,0 +1,9 @@ +digraph { +FirstComponent [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] + FirstComponent -> action1 +action1 -> action2 +action3 [color=green, fillcolor=green, fontcolor=white, style=filled] +action1 -> action3 +firstReducer [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] + action3 -> firstReducer +} diff --git a/docs/examples/case1/action3.svg b/docs/examples/case1/action3.svg new file mode 100644 index 0000000..f34838e --- /dev/null +++ b/docs/examples/case1/action3.svg @@ -0,0 +1,66 @@ + + + + + + + + + +FirstComponent + +FirstComponent + + + +action1 + +action1 + + + +FirstComponent->action1 + + + + + +action2 + +action2 + + + +action1->action2 + + + + + +action3 + +action3 + + + +action1->action3 + + + + + +firstReducer + +firstReducer + + + +action3->firstReducer + + + + + diff --git a/docs/examples/case1/ngrx-graph.json b/docs/examples/case1/ngrx-graph.json new file mode 100644 index 0000000..660ecdc --- /dev/null +++ b/docs/examples/case1/ngrx-graph.json @@ -0,0 +1,15 @@ +{ + "allActions": [ + { "name": "nestedAction1", "nested": true }, + { "name": "nestedAction2", "nested": true }, + { "name": "action1", "nested": false }, + { "name": "action2", "nested": false }, + { "name": "action3", "nested": false } + ], + "loadedActions": [], + "fromComponents": { "FirstComponent": ["action1"] }, + "fromEffects": { + "effect1$": { "input": ["action1"], "output": ["action2", "action3"] } + }, + "fromReducers": { "firstReducer": ["action3"] } +} diff --git a/docs/examples/case2/action1.dot b/docs/examples/case2/action1.dot new file mode 100644 index 0000000..3426226 --- /dev/null +++ b/docs/examples/case2/action1.dot @@ -0,0 +1,14 @@ +digraph { +FirstComponent [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] +action1 [color=green, fillcolor=green, fontcolor=white, style=filled] + FirstComponent -> action1 +nestedAction1 [color=black, fillcolor=lightcyan, fontcolor=black, style=filled] +action1 -> nestedAction1 +action1 -> action3 +nestedAction2 [color=black, fillcolor=lightcyan, fontcolor=black, style=filled] +nestedAction1 -> nestedAction2 +action2 [fillcolor=linen, style=filled] +nestedAction1 -> action2 [arrowhead=dot] +firstReducer [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] + action3 -> firstReducer +} diff --git a/docs/examples/case2/action1.svg b/docs/examples/case2/action1.svg new file mode 100644 index 0000000..574fa97 --- /dev/null +++ b/docs/examples/case2/action1.svg @@ -0,0 +1,90 @@ + + + + + + + + + +FirstComponent + +FirstComponent + + + +action1 + +action1 + + + +FirstComponent->action1 + + + + + +nestedAction1 + +nestedAction1 + + + +action1->nestedAction1 + + + + + +action3 + +action3 + + + +action1->action3 + + + + + +nestedAction2 + +nestedAction2 + + + +nestedAction1->nestedAction2 + + + + + +action2 + +action2 + + + +nestedAction1->action2 + + + + + +firstReducer + +firstReducer + + + +action3->firstReducer + + + + + diff --git a/docs/examples/case2/action3.dot b/docs/examples/case2/action3.dot new file mode 100644 index 0000000..1107ebb --- /dev/null +++ b/docs/examples/case2/action3.dot @@ -0,0 +1,12 @@ +digraph { +FirstComponent [shape="box", color=blue, fillcolor=blue, fontcolor=white, style=filled] + FirstComponent -> action1 +nestedAction1 [color=black, fillcolor=lightcyan, fontcolor=black, style=filled] +action1 -> nestedAction1 +action3 [color=green, fillcolor=green, fontcolor=white, style=filled] +action1 -> action3 +action2 [fillcolor=linen, style=filled] +nestedAction1 -> action2 [arrowhead=dot] +firstReducer [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] + action3 -> firstReducer +} diff --git a/docs/examples/case2/action3.svg b/docs/examples/case2/action3.svg new file mode 100644 index 0000000..53d8b56 --- /dev/null +++ b/docs/examples/case2/action3.svg @@ -0,0 +1,78 @@ + + + + + + + + + +FirstComponent + +FirstComponent + + + +action1 + +action1 + + + +FirstComponent->action1 + + + + + +nestedAction1 + +nestedAction1 + + + +action1->nestedAction1 + + + + + +action3 + +action3 + + + +action1->action3 + + + + + +action2 + +action2 + + + +nestedAction1->action2 + + + + + +firstReducer + +firstReducer + + + +action3->firstReducer + + + + + diff --git a/docs/examples/case2/ngrx-graph.json b/docs/examples/case2/ngrx-graph.json new file mode 100644 index 0000000..a9c8788 --- /dev/null +++ b/docs/examples/case2/ngrx-graph.json @@ -0,0 +1,15 @@ +{ + "allActions": [ + { "name": "nestedAction", "nested": true }, + { "name": "action1", "nested": false }, + { "name": "action2", "nested": false }, + { "name": "action3", "nested": false } + ], + "loadedActions": [{ "name": "nestedAction", "payloadActions": ["action2"] }], + "fromComponents": { "FirstComponent": ["action1"] }, + "fromEffects": { + "effect1$": { "input": ["action1"], "output": ["nestedAction", "action3"] }, + "effect2$": { "input": ["nestedAction"], "output": [] } + }, + "fromReducers": { "firstReducer": ["action3"] } +} diff --git a/docs/keys/action.png b/docs/keys/action.png new file mode 100644 index 0000000000000000000000000000000000000000..0548f1806cf732ed9698aa078ba02954e7476a0e GIT binary patch literal 9422 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 PgjbMNm8p_?6Y_rmZ5*24 literal 0 HcmV?d00001 diff --git a/docs/keys/component.png b/docs/keys/component.png new file mode 100644 index 0000000000000000000000000000000000000000..39d2c37302fb27353b3850021448d027b9bd8d50 GIT binary patch literal 10498 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 Hu0mjfcl9OM literal 0 HcmV?d00001 diff --git a/docs/keys/reducer.png b/docs/keys/reducer.png new file mode 100644 index 0000000000000000000000000000000000000000..666f89217a8b5d2aa36611a3f39defb529d53025 GIT binary patch literal 13413 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 bVBCx9;osgC#rpj@TL+MrR*|ZaFbVm8a=+LE literal 0 HcmV?d00001 diff --git a/docs/keys/selectedAction.png b/docs/keys/selectedAction.png new file mode 100644 index 0000000000000000000000000000000000000000..73c9baa6815df7320368949c4d80155d38821fe8 GIT binary patch literal 9900 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%0AFAeD8$_?2CaiMr&J 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 g$)Qid1^$LObYP}Ms2G*|^NB@TTtTcx action.name)) { CliUx.ux.action.start(` ⚡️ ${_action} `); gen.generateActionGraph( _action, diff --git a/src/generator.ts b/src/generator.ts index 1b11e31..a52851c 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -16,6 +16,8 @@ import { forEachChild, ClassDeclaration, PropertyAccessExpression, + TypeLiteralNode, + PropertySignature, } from 'typescript'; import { join } from 'node:path'; import { isEmpty, uniq } from 'lodash'; @@ -32,6 +34,16 @@ interface ActionsMap { [k: string]: string[]; } +interface TypedAction { + name: string; + nested: boolean; +} + +interface LoadedAction { + name: string; + payloadActions: string[]; +} + export class Generator { private srcDir = ''; private outputDir = ''; @@ -41,7 +53,9 @@ export class Generator { private fromEffects: EffectsStructure | undefined; private fromComponents: ActionsMap | undefined; private fromReucers: ActionsMap | undefined; - allActions: string[]; + allActions: TypedAction[]; + nestedActions: string[]; + loadedActions: LoadedAction[]; constructor( srcDir: string, @@ -55,9 +69,13 @@ export class Generator { this.structureFile = join(this.outputDir, structureFile); const content = force ? undefined : this.readStructure(); this.allActions = content?.allActions ?? this.getAllActions(); + this.nestedActions = this.allActions + .filter(action => action.nested) + .map(action => action.name); this.fromComponents = content?.fromComponents; this.fromEffects = content?.fromEffects; this.fromReucers = content?.fromReducers; + this.loadedActions = content?.loadedActions ?? []; } getParentNodes(node: Node, identifiers: string[]): Node[] { @@ -79,18 +97,38 @@ export class Generator { return nodes; } - getAllActions(): string[] { + getAllActions(): TypedAction[] { const allActions = fglob .sync(join(this.srcDir, '**/*.actions.ts')) - .reduce((result: string[], filename: string) => { - const actionPerFile = this.getParentNodes(readSourceFile(filename), [ - 'createAction', - ]).map(node => - ( + .reduce((result: TypedAction[], filename: string) => { + const actionsPerFile: TypedAction[] = this.getParentNodes( + readSourceFile(filename), + ['createAction'], + ).map(node => { + const name = ( (node.parent as VariableDeclaration).name as Identifier - ).escapedText.toString(), - ); - return [...result, ...actionPerFile]; + ).escapedText.toString(); + const members = ( + ( + (node as CallExpression).arguments[1] as + | CallExpression + | undefined + )?.typeArguments![0] as TypeLiteralNode | undefined + )?.members; + const nested = + (node as CallExpression).arguments.length > 1 && + members + ?.map(member => + ( + (member as PropertySignature).type as any + ).typeName?.escapedText?.toString(), + ) + .includes('Action') === true; + + const action = { name, nested }; + return action; + }); + return [...result, ...actionsPerFile]; }, []); return allActions; } @@ -136,6 +174,81 @@ export class Generator { ); } + updateLoadedActions( + actions: string[], + sourceFile: SourceFile, + parentNode?: Node, + ): string[] { + let result = [...actions]; + const nodeInUse = parentNode ?? sourceFile; + for (const node of getChildNodesRecursivly(nodeInUse)) { + if ( + // get triggered nested actions + node.kind === SyntaxKind.CallExpression && + ((node as CallExpression).expression as Identifier | undefined) && + ((node as CallExpression).expression as Identifier | undefined) + ?.escapedText && + this.nestedActions.includes( + ( + (node as CallExpression).expression as Identifier + ).escapedText.toString(), + ) + ) { + const actionName = ( + (node as CallExpression).expression as Identifier + ).escapedText.toString(); + const payloadActions = this.allActions + .filter((action: TypedAction) => { + return node.getText().match(new RegExp(`[^\\w]${action.name}\\(`)); + }) + .map(action => action.name); + if (!isEmpty(payloadActions)) { + this.loadedActions = [ + ...this.loadedActions, + { name: actionName, payloadActions }, + ]; + } + + for (const payloadAction of payloadActions) { + if ( + nodeInUse + .getText() + .match(new RegExp(`[^\\w]${payloadAction}\\(`, 'g'))?.length === 1 + ) { + result = result.filter(action => !payloadActions.includes(action)); + } + } + } + + if ( + node.kind === SyntaxKind.CallExpression && + ((node as CallExpression).expression as PropertyAccessExpression).name + ) { + const privateMethodName = ( + (node as CallExpression).expression as PropertyAccessExpression + ).name.escapedText.toString(); + const privateMethodActionsAsArguments = ( + (node as CallExpression).arguments as any + ) + .filter( + (arg: Node) => + arg.kind === SyntaxKind.Identifier && + this.allActions + .map(_action => _action.name) + .includes((arg as Identifier).escapedText.toString()), + ) + .map((arg: Identifier) => arg.escapedText.toString()); + result = [ + ...result, + ...privateMethodActionsAsArguments, + ...this.getActionsFromPrivateMethod(sourceFile, privateMethodName), + ]; + } + } + + return result; + } + effectDispatchedActions( effect: Node, sourceFile: SourceFile, @@ -154,27 +267,15 @@ export class Generator { )) { actions = [ ...actions, - ...this.allActions.filter((action: string) => - mapNode.getText().match(new RegExp(`[^\\w]${action}\\(`)), - ), + ...this.allActions + .filter((action: TypedAction) => + mapNode.getText().match(new RegExp(`[^\\w]${action.name}\\(`)), + ) + .map(action => action.name), ]; } - for (const node of getChildNodesRecursivly(effect)) { - if ( - node.kind === SyntaxKind.CallExpression && - ((node as CallExpression).expression as PropertyAccessExpression).name - ) { - const privateMethodName = ( - (node as CallExpression).expression as PropertyAccessExpression - ).name.escapedText.toString(); - actions = [ - ...actions, - ...this.getActionsFromPrivateMethod(sourceFile, privateMethodName), - ]; - } - } - + actions = this.updateLoadedActions(actions, sourceFile, effect); return [...new Set(actions.filter(action => !input.includes(action)))]; } @@ -188,11 +289,13 @@ export class Generator { if (callable.kind === SyntaxKind.MethodDeclaration) { actions = [ ...actions, - ...this.allActions.filter((action: string) => { - return callable - .getText() - .match(new RegExp(`[^\\w]${action}[^\\w]`)); - }), + ...this.allActions + .filter((action: TypedAction) => { + return callable + .getText() + .match(new RegExp(`[^\\w]${action.name}\\(`)); + }) + .map(action => action.name), ]; } } @@ -241,15 +344,19 @@ export class Generator { const nodes = this.getParentNodes(sourceFile, ['dispatch']).map(node => node.parent.getText(), ); - const actions = [ + let actions = [ ...new Set( - this.allActions.filter( - (action: string) => - nodes.filter(node => node.match(new RegExp(`[^\\w]${action}\\(`))) - .length, - ), + this.allActions + .filter( + (action: TypedAction) => + nodes.filter(node => + node.match(new RegExp(`[^\\w]${action.name}\\(`)), + ).length, + ) + .map(action => action.name), ), ]; + actions = this.updateLoadedActions(actions, sourceFile); return { [className]: actions }; } @@ -274,7 +381,8 @@ export class Generator { readStructure(): | { - allActions: string[]; + allActions: TypedAction[]; + loadedActions: LoadedAction[]; fromComponents: ActionsMap; fromEffects: EffectsStructure; fromReducers: ActionsMap; @@ -307,6 +415,7 @@ export class Generator { const content = JSON.stringify({ allActions: this.allActions, + loadedActions: this.loadedActions, fromComponents, fromEffects, fromReducers, @@ -327,6 +436,9 @@ export class Generator { ...chainActionsByInput(fromEffects, action), ...chainActionsByOutput(fromEffects, action), ]; + + const selectedActionStyle = + '[color=green, fillcolor=green, fontcolor=white, style=filled]'; let content = 'digraph {\n'; for (const [k, v] of Object.entries(fromComponents)) { const lines = v.map(componentAction => { @@ -345,6 +457,26 @@ export class Generator { content += lines.join(''); } + for (const v of filterdByAction) { + const lines = v.output.map(o => `${v.input} -> ${o}\n`); + content += lines.join(''); + } + + const filterdLoadedActions = this.loadedActions.filter(a => + [a.name, ...a.payloadActions].some(_action => + filterdByAction + .flatMap(b => [...b.input, ...b.output]) + .includes(_action), + ), + ); + const lines = filterdLoadedActions.map(a => { + const style = a.payloadActions.includes(action) + ? selectedActionStyle + : '[fillcolor=linen, style=filled]'; + return `${a.payloadActions} ${style}\n${a.name} -> ${a.payloadActions} [arrowhead=dot]\n`; + }); + content += lines.join(''); + for (const [k, v] of Object.entries(fromReducers)) { const lines = v.map(reducerAction => { if ( @@ -362,16 +494,18 @@ export class Generator { content += lines.join(''); } - for (const v of filterdByAction) { - const lines = v.output.map(o => `${v.input} -> ${o}\n`); - content += lines.join(''); - } - content += '}\n'; content = content.replace( new RegExp(`([^\n]*${action}[^\n]*)`), - `${action} [color=green, fillcolor=green, fontcolor=white, style=filled]\n$1`, + `${action} ${selectedActionStyle}\n$1`, ); + for (const action of this.nestedActions) { + content = content.replace( + new RegExp(`([^\n]*${action}[^\n]*)`), + `${action} [color=black, fillcolor=lightcyan, fontcolor=black, style=filled]\n$1`, + ); + } + fs.writeFileSync(dotFile, content); } @@ -397,6 +531,12 @@ export class Generator { content += lines.join(''); } + const lines = this.loadedActions.map( + a => + `${a.payloadActions} [fillcolor=linen, style=filled]\n${a.name} -> ${a.payloadActions} [arrowhead=dot]\n`, + ); + content += lines.join(''); + for (const [k, v] of Object.entries(fromReducers)) { const lines = v.map( o => `${k} [shape="hexagon", color=purple, fillcolor=purple, fontcolor=white, style=filled] From 82512d963082d553035e45d3d8557ade2eee6946 Mon Sep 17 00:00:00 2001 From: Ammar Najjar Date: Tue, 27 Dec 2022 22:08:23 +0100 Subject: [PATCH 2/3] docs: fix typos --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d38a1d7..db9acaf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Motivation: -Working with a very big [NgRx](https://ngrx.io/) store in an angular application will lead to having lots of actions/effects and lots of interactions betwen components/actions/reducers. It gets very tedious very quickly to follow an action from the start to the end, and it is very easy to miss an action dispatched in an effect somewhere along the chain of actions. +Working with a very big [NgRx](https://ngrx.io/) store in an angular application will lead to having lots of actions/effects and lots of interactions between components/actions/reducers. It gets very tedious very quickly to follow an action from the start to the end, and it is very easy to miss an action dispatched in an effect somewhere along the chain of actions. This packages, tries to collect all actions/components/reducers participating in a particular flow and generate dot files for that flow, with the idea that following a graph visually is easier than following effects and actions in code. @@ -41,7 +41,7 @@ If this file exists, source code will not be parsed for actions, the recorded st ### Input: ```typescript -// declarations +// actions export const action1 = createAction('Action1'); export const action2 = createAction('Action2'); export const action3 = createAction('Action3'); @@ -102,7 +102,7 @@ npx ngrx-graph action3 ### Input: ```typescript -// declarations +// actions export const nestedAction = createAction( 'NestedAction', props<{ action: Action }>(), @@ -263,6 +263,6 @@ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.1 # Status: -This project is still young and encourage collaborations. If you have an ideas/questions/fixes please do not hesitate to open an issue or provide a pull request. +This project is still young and it encourages collaborations. If you have an ideas/questions/fixes please do not hesitate to open an issue or provide a pull request. I work on this on my own free time only. From 5ad5ca9ce433f4425d4459b302da8807feb0da97 Mon Sep 17 00:00:00 2001 From: Ammar Najjar Date: Tue, 27 Dec 2022 22:09:52 +0100 Subject: [PATCH 3/3] chore: pump version --- README.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index db9acaf..d71ca5f 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ $ npm install -g ngrx-graph $ ngrx-graph COMMAND running command... $ ngrx-graph (--version) -ngrx-graph/0.0.8 darwin-arm64 node-v19.3.0 +ngrx-graph/0.0.9 darwin-arm64 node-v19.3.0 $ ngrx-graph --help [COMMAND] USAGE $ ngrx-graph COMMAND @@ -236,7 +236,7 @@ EXAMPLES $ ngrx-graph graph ``` -_See code: [dist/commands/graph/index.ts](https://github.com/ammarnajjar/ngrx-graph/blob/v0.0.8/dist/commands/graph/index.ts)_ +_See code: [dist/commands/graph/index.ts](https://github.com/ammarnajjar/ngrx-graph/blob/v0.0.9/dist/commands/graph/index.ts)_ ## `ngrx-graph help [COMMAND]` diff --git a/package.json b/package.json index f6365f7..fd2b755 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngrx-graph", - "version": "0.0.8", + "version": "0.0.9", "description": "Generate NgRx actions graph", "author": "Ammar Najjar", "bin": {