feat(schematics): update ngrx to 7.2.0

This commit is contained in:
Jason Jean 2019-01-16 20:54:13 -05:00 committed by Victor Savkin
parent e2f482afd3
commit dbf59af6a5
8 changed files with 592 additions and 76 deletions

View File

@ -33,11 +33,11 @@
"@angular/platform-browser-dynamic": "^7.2.1",
"@angular/router": "^7.2.1",
"@angular/upgrade": "^7.2.1",
"@ngrx/effects": "6.1.2",
"@ngrx/router-store": "6.1.2",
"@ngrx/schematics": "6.1.2",
"@ngrx/store": "6.1.2",
"@ngrx/store-devtools": "6.1.2",
"@ngrx/effects": "7.1.0",
"@ngrx/router-store": "7.1.0",
"@ngrx/schematics": "7.1.0",
"@ngrx/store": "7.1.0",
"@ngrx/store-devtools": "7.1.0",
"@schematics/angular": "~7.2.2",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",

View File

@ -157,15 +157,17 @@ describe('DataPersistence', () => {
const root = TestBed.createComponent(RootCmp);
const router: Router = TestBed.get(Router);
let action;
TestBed.get(Actions).subscribe(a => (action = a));
let actions: any[] = [];
TestBed.get(Actions).subscribe((a: any) => actions.push(a));
router.navigateByUrl('/todo/123');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).not.toContain('ID 123');
expect(action.type).toEqual('ERROR');
expect(action.payload.error.message).toEqual('boom');
expect(actions.map(a => a.type)).toContain('ERROR');
expect(
actions.find(a => a.type === 'ERROR').payload.error.message
).toEqual('boom');
// can recover after an error
router.navigateByUrl('/todo/456');
@ -205,15 +207,17 @@ describe('DataPersistence', () => {
const root = TestBed.createComponent(RootCmp);
const router: Router = TestBed.get(Router);
let action;
TestBed.get(Actions).subscribe(a => (action = a));
let actions: any[] = [];
TestBed.get(Actions).subscribe((a: any) => actions.push(a));
router.navigateByUrl('/todo/123');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).not.toContain('ID 123');
expect(action.type).toEqual('ERROR');
expect(action.payload.error).toEqual('boom');
expect(actions.map(a => a.type)).toContain('ERROR');
expect(actions.find(a => a.type === 'ERROR').payload.error).toEqual(
'boom'
);
router.navigateByUrl('/todo/456');
tick(0);

View File

@ -57,7 +57,7 @@
},
"update-7.6.0": {
"version": "7.6.0",
"description": "Add VSCode Extensions",
"description": "Add VSCode Extensions and Update NgRx",
"factory": "./update-7-6-0/update-7-6-0"
}
}

View File

@ -1,24 +1,96 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import {
SchematicTestRunner,
UnitTestTree
} from '@angular-devkit/schematics/testing';
import * as path from 'path';
import { serializeJson } from '../../src/utils/fileutils';
import { readJsonInTree, updateJsonInTree } from '../../src/utils/ast-utils';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
const effectContents = `
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';
import { UserPartialState } from './user.reducer';
import {
LoadUser,
UserLoaded,
UserLoadError,
UserActionTypes
} from './user.actions';
@Injectable()
export class UserEffects {
@Effect() effect$ = this.actions$.ofType(LoadUser).pipe(mapTo(UserLoaded));
@Effect() effect2$ = this.actions$.ofType<UserLoaded>(LoadUser).pipe(mapTo(UserLoaded));
@Effect() effect3$ = this.actions$.ofType<UserLoaded>(LoadUser).pipe(withLatestFrom(this.store.select(selector)), mapTo(UserLoaded));
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<UserPartialState>,
private store: Store<AppState>
) {}
}
`;
const selectorContents = `
import { Store } from '@ngrx/store';
import { Component } from '@angular/core';
import { AppState, selector } from '../+state';
@Component({
selector: 'app',
template: '',
styles: []
})
export class AppComponent {
slice$ = this.store.select(selector).pipe(
map(a => a)
);
slice2$: Observable<string>;
slice3$ = Observable.from([]).pipe(
withLatestFrom(this.store.select(selector5))
);
constructor(
private store: Store<AppState>
) {}
ngOnInit() {
this.slice2$ = this.store.select(selector2);
this.store.select(selector3).subscribe(console.log);
}
}
`;
describe('Update 7.6.0', () => {
let initialTree: Tree;
let schematicRunner: SchematicTestRunner;
beforeEach(() => {
initialTree = Tree.empty();
initialTree = new UnitTestTree(Tree.empty());
initialTree.create(
'package.json',
serializeJson({
dependencies: {
'@ngrx/effects': '6.1.2',
'@ngrx/router-store': '6.1.2',
'@ngrx/store': '6.1.2'
},
devDependencies: {
'@angular/cli': '7.1.0',
typescript: '~3.1.0'
'@ngrx/schematics': '6.1.2',
'@ngrx/store-devtools': '6.1.2'
}
})
);
@ -29,41 +101,125 @@ describe('Update 7.6.0', () => {
);
});
it('should add vscode extension recommendations', async () => {
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
describe('VSCode Extension Recommendations', () => {
it('should be added', async () => {
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
expect(readJsonInTree(result, '.vscode/extensions.json')).toEqual({
recommendations: [
'nrwl.angular-console',
'angular.ng-template',
'esbenp.prettier-vscode'
]
expect(readJsonInTree(result, '.vscode/extensions.json')).toEqual({
recommendations: [
'nrwl.angular-console',
'angular.ng-template',
'esbenp.prettier-vscode'
]
});
});
it('should be added to existing recommendations', async () => {
initialTree = await schematicRunner
.callRule(
updateJsonInTree('.vscode/extensions.json', () => ({
recommendations: ['eamodio.gitlens', 'angular.ng-template']
})),
initialTree
)
.toPromise();
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
expect(readJsonInTree(result, '.vscode/extensions.json')).toEqual({
recommendations: [
'eamodio.gitlens',
'angular.ng-template',
'nrwl.angular-console',
'esbenp.prettier-vscode'
]
});
});
});
it('should add to existing vscode extension recommendations', async () => {
initialTree = await schematicRunner
.callRule(
updateJsonInTree('.vscode/extensions.json', () => ({
recommendations: ['eamodio.gitlens', 'angular.ng-template']
})),
initialTree
)
.toPromise();
describe('NgRx Migration', () => {
it('should update ngrx to 7.1.0', async () => {
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
expect(readJsonInTree(result, 'package.json')).toEqual({
dependencies: {
'@ngrx/effects': '7.2.0',
'@ngrx/router-store': '7.2.0',
'@ngrx/store': '7.2.0'
},
devDependencies: {
'@ngrx/schematics': '7.2.0',
'@ngrx/store-devtools': '7.2.0'
}
});
});
expect(readJsonInTree(result, '.vscode/extensions.json')).toEqual({
recommendations: [
'eamodio.gitlens',
'angular.ng-template',
'nrwl.angular-console',
'esbenp.prettier-vscode'
]
it('should convert ofType code', async () => {
initialTree.create('user.effects.ts', effectContents);
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
const contents = result.readContent('user.effects.ts');
expect(contents).toContain(
"import { Effect, Actions, ofType } from '@ngrx/effects';"
);
expect(stripIndents`${contents}`).toContain(
stripIndents`
@Effect() effect$ = this.actions$.pipe(
ofType(LoadUser),
mapTo(UserLoaded)
);`
);
expect(stripIndents`${contents}`).toContain(
stripIndents`
@Effect() effect2$ = this.actions$.pipe(
ofType<UserLoaded>(LoadUser),
mapTo(UserLoaded)
);`
);
expect(stripIndents`${contents}`).toContain(
stripIndents`
@Effect() effect3$ = this.actions$.pipe(
ofType<UserLoaded>(LoadUser),
withLatestFrom(this.store.pipe(select(selector))),
mapTo(UserLoaded)
);`
);
});
it('should convert select code', async () => {
initialTree.create('app.component.ts', selectorContents);
const result = await schematicRunner
.runSchematicAsync('update-7.6.0', {}, initialTree)
.toPromise();
const contents = result.readContent('app.component.ts');
expect(contents).toContain(
"import { Store, select } from '@ngrx/store';"
);
expect(stripIndents`${contents}`).toContain(
stripIndents`
slice$ = this.store.pipe(
select(selector),
map(a => a)
);`
);
expect(contents).toContain(
'this.slice2$ = this.store.pipe(select(selector2))'
);
expect(contents).toContain(
'this.store.pipe(select(selector3)).subscribe(console.log);'
);
expect(stripIndents`${contents}`).toContain(stripIndents`
slice3$ = Observable.from([]).pipe(
withLatestFrom(this.store.pipe(select(selector5)))
);`);
});
});
});

View File

@ -1,6 +1,21 @@
import { Rule, chain, externalSchematic } from '@angular-devkit/schematics';
import {
Rule,
chain,
SchematicContext,
Tree
} from '@angular-devkit/schematics';
import { updateJsonInTree } from '../../src/utils/ast-utils';
import { ReplaceChange } from '@schematics/angular/utility/change';
import { getSourceNodes } from '@schematics/angular/utility/ast-utils';
import * as ts from 'typescript';
import {
updateJsonInTree,
readJsonInTree,
insert
} from '../../src/utils/ast-utils';
import { formatFiles } from '../../src/utils/rules/format-files';
const addExtensionRecommendations = updateJsonInTree(
'.vscode/extensions.json',
@ -20,6 +35,339 @@ const addExtensionRecommendations = updateJsonInTree(
}
);
export default function(): Rule {
return chain([addExtensionRecommendations]);
function addItemToImport(
path: string,
sourceFile: ts.SourceFile,
printer: ts.Printer,
importStatement: ts.ImportDeclaration,
symbol: string
) {
const newImport = ts.createImportDeclaration(
importStatement.decorators,
importStatement.modifiers,
ts.createImportClause(
importStatement.importClause.name,
ts.createNamedImports([
...(importStatement.importClause.namedBindings as ts.NamedImports)
.elements,
ts.createImportSpecifier(undefined, ts.createIdentifier(symbol))
])
),
importStatement.moduleSpecifier
);
return new ReplaceChange(
path,
importStatement.getStart(sourceFile),
importStatement.getText(sourceFile),
printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile)
);
}
function isEffectDecorator(decorator: ts.Decorator) {
return (
ts.isCallExpression(decorator.expression) &&
ts.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.text === 'Effect'
);
}
function getImport(sourceFile: ts.SourceFile, path: string, symbol: string) {
return sourceFile.statements
.filter(ts.isImportDeclaration)
.filter(statement =>
statement.moduleSpecifier.getText(sourceFile).includes(path)
)
.find(statement => {
if (!ts.isNamedImports(statement.importClause.namedBindings)) {
return false;
}
return statement.importClause.namedBindings.elements.some(
element => element.getText(sourceFile) === symbol
);
});
}
function updateOfTypeCode(path: string, sourceFile: ts.SourceFile) {
const effectsImport = getImport(sourceFile, '@ngrx/effects', 'Effect');
if (!effectsImport) {
return [];
}
const effects: ts.PropertyDeclaration[] = [];
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
sourceFile.statements
.filter(ts.isClassDeclaration)
.map(clazz =>
clazz.members
.filter(ts.isPropertyDeclaration)
.filter(
member =>
member.decorators && member.decorators.some(isEffectDecorator)
)
)
.forEach(properties => {
effects.push(...properties);
});
effects.forEach(effect => {
if (
ts.isCallExpression(effect.initializer) &&
ts.isPropertyAccessExpression(effect.initializer.expression) &&
effect.initializer.expression.name.text === 'pipe' &&
ts.isCallExpression(effect.initializer.expression.expression) &&
ts.isPropertyAccessExpression(
effect.initializer.expression.expression.expression
) &&
effect.initializer.expression.expression.expression.name.text === 'ofType'
) {
const originalText = effect.initializer.getText(sourceFile);
const ofTypeExpression = ts.createCall(
ts.createIdentifier('ofType'),
effect.initializer.expression.expression.typeArguments,
effect.initializer.expression.expression.arguments
);
const node = ts.createCall(
ts.createPropertyAccess(
effect.initializer.expression.expression.expression.expression,
'pipe'
),
effect.initializer.typeArguments,
ts.createNodeArray([
ofTypeExpression,
...(effect.initializer as ts.CallExpression).arguments
])
);
const newEffect = printer.printNode(
ts.EmitHint.Expression,
node,
sourceFile
);
const change = new ReplaceChange(
path,
effect.initializer.getStart(sourceFile),
originalText,
newEffect
);
changes.push(change);
}
});
if (changes.length > 0) {
changes.unshift(
addItemToImport(path, sourceFile, printer, effectsImport, 'ofType')
);
}
return changes;
}
function getConstructor(
classDeclaration: ts.ClassDeclaration
): ts.ConstructorDeclaration {
return classDeclaration.members.find(ts.isConstructorDeclaration);
}
function getStoreProperty(
sourceFile: ts.SourceFile,
constructor: ts.ConstructorDeclaration
): string {
const storeParameter = constructor.parameters.find(
parameter =>
parameter.type && parameter.type.getText(sourceFile).includes('Store')
);
return storeParameter ? storeParameter.name.getText(sourceFile) : null;
}
function updateSelectorCode(path: string, sourceFile: ts.SourceFile) {
const storeImport = getImport(sourceFile, '@ngrx/store', 'Store');
if (!storeImport) {
return [];
}
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
sourceFile.statements
.filter(ts.isClassDeclaration)
.forEach(classDeclaration => {
const constructor = getConstructor(classDeclaration);
if (!constructor) {
return;
}
const storeProperty = getStoreProperty(sourceFile, constructor);
getSourceNodes(sourceFile).forEach(node => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isPropertyAccessExpression(node.expression.expression) &&
ts.isIdentifier(node.expression.name) &&
ts.isIdentifier(node.expression.expression.name) &&
node.expression.name.getText(sourceFile) === 'select' &&
node.expression.expression.name.getText(sourceFile) ===
storeProperty &&
node.expression.expression.expression.kind ===
ts.SyntaxKind.ThisKeyword
) {
const newExpression = ts.createCall(
ts.createPropertyAccess(
ts.createPropertyAccess(
ts.createIdentifier('this'),
ts.createIdentifier(storeProperty)
),
ts.createIdentifier('pipe')
),
[],
[
ts.createCall(
ts.createIdentifier('select'),
node.typeArguments,
node.arguments
)
]
);
const newNode = printer.printNode(
ts.EmitHint.Expression,
newExpression,
sourceFile
);
changes.push(
new ReplaceChange(
path,
node.getStart(sourceFile),
node.getText(sourceFile),
newNode
)
);
}
});
});
if (changes.length > 0) {
changes.unshift(
addItemToImport(path, sourceFile, printer, storeImport, 'select')
);
}
return changes;
}
function migrateNgrx(host: Tree, context: SchematicContext) {
const ngrxVersion = readJsonInTree(host, 'package.json').dependencies[
'@ngrx/store'
];
if (
!(
ngrxVersion.startsWith('6.') ||
ngrxVersion.startsWith('~6.') ||
ngrxVersion.startsWith('^6.')
)
) {
return host;
}
host.visit(path => {
if (!path.endsWith('.ts')) {
return;
}
let sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
if (sourceFile.isDeclarationFile) {
return;
}
insert(host, path, updateOfTypeCode(path, sourceFile));
sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
insert(host, path, updateSelectorCode(path, sourceFile));
sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
insert(host, path, cleanUpDoublePipes(path, sourceFile));
});
}
function cleanUpDoublePipes(
path: string,
sourceFile: ts.SourceFile
): ReplaceChange[] {
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
getSourceNodes(sourceFile).forEach(node => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isCallExpression(node.expression.expression) &&
ts.isPropertyAccessExpression(node.expression.expression.expression) &&
node.expression.name.text === 'pipe' &&
node.expression.expression.expression.name.text === 'pipe'
) {
const singlePipe = ts.createCall(
node.expression.expression.expression,
node.typeArguments,
[...node.expression.expression.arguments, ...node.arguments]
);
changes.push(
new ReplaceChange(
path,
node.getStart(sourceFile),
node.getText(sourceFile),
printer.printNode(ts.EmitHint.Expression, singlePipe, sourceFile)
)
);
}
});
return changes;
}
const updateNgrx = updateJsonInTree('package.json', json => {
json.devDependencies = json.devDependencies || {};
json.dependencies = json.dependencies || {};
json.dependencies = {
...json.dependencies,
'@ngrx/effects': '7.2.0',
'@ngrx/router-store': '7.2.0',
'@ngrx/store': '7.2.0'
};
json.devDependencies = {
...json.devDependencies,
'@ngrx/schematics': '7.2.0',
'@ngrx/store-devtools': '7.2.0'
};
return json;
});
export default function(): Rule {
return chain([
addExtensionRecommendations,
migrateNgrx,
updateNgrx,
formatFiles()
]);
}

View File

@ -2,7 +2,7 @@ export const angularCliVersion = '~7.2.2';
export const angularVersion = '^7.0.0';
export const angularDevkitVersion = '~0.11.2';
export const angularJsVersion = '1.6.6';
export const ngrxVersion = '6.1.2';
export const ngrxVersion = '7.2.0';
export const ngrxStoreFreezeVersion = '0.2.4';
export const nxVersion = '*';
export const schematicsVersion = '*';

View File

@ -248,7 +248,7 @@ export function removeFromNgModule(
return [
new RemoveChange(
modulePath,
matchingProperty.pos,
matchingProperty.getStart(source),
matchingProperty.getFullText(source)
)
];
@ -545,6 +545,9 @@ export function addGlobal(
}
export function insert(host: Tree, modulePath: string, changes: Change[]) {
if (changes.length < 1) {
return;
}
const recorder = host.beginUpdate(modulePath);
for (const change of changes) {
if (change instanceof InsertChange) {
@ -555,8 +558,8 @@ export function insert(host: Tree, modulePath: string, changes: Change[]) {
// do nothing
} else if (change instanceof ReplaceChange) {
const action = <any>change;
recorder.remove(action.pos + 1, action.oldText.length);
recorder.insertLeft(action.pos + 1, action.newText);
recorder.remove(action.pos, action.oldText.length);
recorder.insertLeft(action.pos, action.newText);
} else {
throw new Error(`Unexpected Change '${change}'`);
}
@ -941,6 +944,11 @@ export function replaceNodeValue(
content: string
) {
insert(host, modulePath, [
new ReplaceChange(modulePath, node.pos, node.getFullText(), content)
new ReplaceChange(
modulePath,
node.getStart(node.getSourceFile()),
node.getFullText(),
content
)
]);
}

View File

@ -225,30 +225,30 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@ngrx/effects@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-6.1.2.tgz#602f3ee9798e00179075ddef030ded88c28b1aa1"
integrity sha512-RUuQ5/7ofxGEZnRRdlC1oE9ugVlTYGm92MVj7c6IirHrVN9W5yQjjMTYEYceVCDOYsiXP7Pyw0dcPp6J5wD2EQ==
"@ngrx/effects@7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-7.1.0.tgz#c42966a92096d605b72a1959599ad6c90a1e15a2"
integrity sha512-0oF9VaixL8TNzIyBfFieLXunoz66uuceGfh5EaepQu+qPcVhnFq5KvYHDQOdCq0uuQYB7rVwNFYjOD7pO5LE5A==
"@ngrx/router-store@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-6.1.2.tgz#63bfcd3710c53ea2c5456b82c57fd433e5af25bc"
integrity sha512-sj083ZYrx0aY+vU/t8Ub0KYDHcMpatXJIOJR/eDNSuH54fPiBM9MrdI3hs/XHoXHxSaHOJoZ7f6I8XcUeptxyA==
"@ngrx/router-store@7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-7.1.0.tgz#931bdc902d806b3d026e99604748ebfefcd82dc3"
integrity sha512-xLTtdLvbgwFyhXU9A/kYsuVruvp8w7WcNorGq0LyWCwI+rk4twaNFJLUx/rVnLbzfXLichU8kHAiRmR8VD438A==
"@ngrx/schematics@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@ngrx/schematics/-/schematics-6.1.2.tgz#52dce7f2d4275791732805576eaf007d1043ef58"
integrity sha512-hiu8rdYAJIfIoJHPlAJr+mBkXab7OLRyg8Z/i5lUpmE382yvESfcVI9hqYsGXWBbyfY5Al+BGzGhBth5gc5AvA==
"@ngrx/schematics@7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@ngrx/schematics/-/schematics-7.1.0.tgz#ddc4faebd506f28ac181df04091b630c70ced713"
integrity sha512-4+drIY4jCMgsOZWo6LTpUL8uLxv8RrbzS5CNEcm8EvwmUrzd/GKoCZ9rKqzoAlF5EyA+1OraMppeOHV0j8DZ3w==
"@ngrx/store-devtools@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-6.1.2.tgz#b9cb8d6bcd7ee0d171dde86c9bdd5e6a5bc5563a"
integrity sha512-hvWMKcRIAtAFb2lb4woRenPHPgOiLFjy8R2PtCiw4uP3WrBVB4JHqUuP230/iRMcU5XmySp+LhNqhkk1zsoUqQ==
"@ngrx/store-devtools@7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-7.1.0.tgz#ea5248d616b2bdf7607e9f4a1c80aa4365e0f830"
integrity sha512-r0LUFQKxKOhFIO2TBK7k6eoOCT6WGQFXzOFTWwctU7teQgSSpzyUVQfsULfAyFpGN9f1hog9leY/J/EVjdwBQQ==
"@ngrx/store@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-6.1.2.tgz#20fb5ab4d79571b804a348093aa11a167fe2946f"
integrity sha512-W9MbXrwhIRmN1BlINF9BT+rHR046e1HNk7GqykcDJrK9wW74PJW3aE5iuPb2sTPipBMjPHsXzc73E4U/+OTAyw==
"@ngrx/store@7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-7.1.0.tgz#072f22e062e264c62859f37d9807dfac860f161d"
integrity sha512-tM8ZGxbLTyQ5JUHow3/PSFU3FWfYJs/wAVyRlxzTxfY7Krs/TJ64GgI4lOa6gVizi2UiYfLtI31pJrxzXyxEoA==
"@ngtools/json-schema@^1.1.0":
version "1.1.0"