From c490bc9e205b6ad0da26711ea517308040a13d6a Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Fri, 28 Jul 2017 13:32:21 -0500 Subject: [PATCH 01/67] chore(ISSUE_TEMPLATE): Comment out OpenCollective donation link (#210) --- .github/ISSUE_TEMPLATE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 34adfdba5e..322a2870b2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,4 @@ + ## I'm submitting a... @@ -27,5 +28,3 @@ http://plnkr.co/edit/tpl:757r6L ## Other information: - -:heartpulse: ngrx? Please consider supporting our collective: 👉 [donate](https://opencollective.com/ngrx/donate) From 9bdfd7048d63a75c84b200fab30ed3e11cab6356 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Fri, 28 Jul 2017 14:30:51 -0500 Subject: [PATCH 02/67] feat(Platform): Introduce @ngrx/entity (#207) --- build/config.ts | 4 + modules/entity/README.md | 6 + modules/entity/index.ts | 7 + modules/entity/package.json | 22 ++ modules/entity/public_api.ts | 1 + modules/entity/rollup.config.js | 10 + modules/entity/spec/entity_state.spec.ts | 33 +++ modules/entity/spec/fixtures/book.ts | 21 ++ .../entity/spec/sorted_state_adapter.spec.ts | 234 ++++++++++++++++++ modules/entity/spec/state_selectors.spec.ts | 57 +++++ .../spec/unsorted_state_adapter.spec.ts | 205 +++++++++++++++ modules/entity/src/create_adapter.ts | 30 +++ modules/entity/src/entity_state.ts | 20 ++ modules/entity/src/index.ts | 2 + modules/entity/src/models.ts | 54 ++++ modules/entity/src/sorted_state_adapter.ts | 132 ++++++++++ modules/entity/src/state_adapter.ts | 16 ++ modules/entity/src/state_selectors.ts | 27 ++ modules/entity/src/unsorted_state_adapter.ts | 93 +++++++ modules/entity/tsconfig-build.json | 33 +++ package.json | 1 + yarn.lock | 82 ++---- 22 files changed, 1032 insertions(+), 58 deletions(-) create mode 100644 modules/entity/README.md create mode 100644 modules/entity/index.ts create mode 100644 modules/entity/package.json create mode 100644 modules/entity/public_api.ts create mode 100644 modules/entity/rollup.config.js create mode 100644 modules/entity/spec/entity_state.spec.ts create mode 100644 modules/entity/spec/fixtures/book.ts create mode 100644 modules/entity/spec/sorted_state_adapter.spec.ts create mode 100644 modules/entity/spec/state_selectors.spec.ts create mode 100644 modules/entity/spec/unsorted_state_adapter.spec.ts create mode 100644 modules/entity/src/create_adapter.ts create mode 100644 modules/entity/src/entity_state.ts create mode 100644 modules/entity/src/index.ts create mode 100644 modules/entity/src/models.ts create mode 100644 modules/entity/src/sorted_state_adapter.ts create mode 100644 modules/entity/src/state_adapter.ts create mode 100644 modules/entity/src/state_selectors.ts create mode 100644 modules/entity/src/unsorted_state_adapter.ts create mode 100644 modules/entity/tsconfig-build.json diff --git a/build/config.ts b/build/config.ts index 29e2da865b..fdda5e781c 100644 --- a/build/config.ts +++ b/build/config.ts @@ -25,4 +25,8 @@ export const packages: PackageDescription[] = [ name: 'store-devtools', hasTestingModule: false, }, + { + name: 'entity', + hasTestingModule: false, + }, ]; diff --git a/modules/entity/README.md b/modules/entity/README.md new file mode 100644 index 0000000000..d26531d598 --- /dev/null +++ b/modules/entity/README.md @@ -0,0 +1,6 @@ +@ngrx/entity +======= + +The sources for this package are in the main [ngrx/platform](https://github.com/ngrx/platform) repo. Please file issues and pull requests against that repo. + +License: MIT diff --git a/modules/entity/index.ts b/modules/entity/index.ts new file mode 100644 index 0000000000..637e1cf2bf --- /dev/null +++ b/modules/entity/index.ts @@ -0,0 +1,7 @@ +/** + * DO NOT EDIT + * + * This file is automatically generated at build + */ + +export * from './public_api'; diff --git a/modules/entity/package.json b/modules/entity/package.json new file mode 100644 index 0000000000..791b17138f --- /dev/null +++ b/modules/entity/package.json @@ -0,0 +1,22 @@ +{ + "name": "@ngrx/entity", + "version": "4.0.1", + "description": "Common utilities for entity reducers", + "module": "@ngrx/entity.es5.js", + "es2015": "@ngrx/entity.js", + "main": "bundles/entity.umd.js", + "typings": "entity.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/ngrx/platform.git" + }, + "authors": [ + "Mike Ryan" + ], + "license": "MIT", + "peerDependencies": { + "@angular/core": "^4.0.0", + "@ngrx/store": "^4.0.0", + "rxjs": "^5.0.0" + } +} diff --git a/modules/entity/public_api.ts b/modules/entity/public_api.ts new file mode 100644 index 0000000000..cba1843545 --- /dev/null +++ b/modules/entity/public_api.ts @@ -0,0 +1 @@ +export * from './src/index'; diff --git a/modules/entity/rollup.config.js b/modules/entity/rollup.config.js new file mode 100644 index 0000000000..bba59155ea --- /dev/null +++ b/modules/entity/rollup.config.js @@ -0,0 +1,10 @@ +export default { + entry: './dist/entity/@ngrx/entity.es5.js', + dest: './dist/entity/bundles/entity.umd.js', + format: 'umd', + exports: 'named', + moduleName: 'ngrx.entity', + globals: { + '@ngrx/store': 'ngrx.store' + } +} diff --git a/modules/entity/spec/entity_state.spec.ts b/modules/entity/spec/entity_state.spec.ts new file mode 100644 index 0000000000..bd61dc9678 --- /dev/null +++ b/modules/entity/spec/entity_state.spec.ts @@ -0,0 +1,33 @@ +import { createEntityAdapter, EntityAdapter } from '../src'; +import { BookModel } from './fixtures/book'; + +describe('Entity State', () => { + let adapter: EntityAdapter; + + beforeEach(() => { + adapter = createEntityAdapter({ + selectId: (book: BookModel) => book.id, + }); + }); + + it('should let you get the initial state', () => { + const initialState = adapter.getInitialState(); + + expect(initialState).toEqual({ + ids: [], + entities: {}, + }); + }); + + it('should let you provide additional initial state properties', () => { + const additionalProperties = { isHydrated: true }; + + const initialState = adapter.getInitialState(additionalProperties); + + expect(initialState).toEqual({ + ...additionalProperties, + ids: [], + entities: {}, + }); + }); +}); diff --git a/modules/entity/spec/fixtures/book.ts b/modules/entity/spec/fixtures/book.ts new file mode 100644 index 0000000000..a2ffcacfdc --- /dev/null +++ b/modules/entity/spec/fixtures/book.ts @@ -0,0 +1,21 @@ +const deepFreeze = require('deep-freeze'); + +export interface BookModel { + id: string; + title: string; +} + +export const AClockworkOrange: BookModel = deepFreeze({ + id: 'aco', + title: 'A Clockwork Orange', +}); + +export const AnimalFarm: BookModel = deepFreeze({ + id: 'af', + title: 'Animal Farm', +}); + +export const TheGreatGatsby: BookModel = deepFreeze({ + id: 'tgg', + title: 'The Great Gatsby', +}); diff --git a/modules/entity/spec/sorted_state_adapter.spec.ts b/modules/entity/spec/sorted_state_adapter.spec.ts new file mode 100644 index 0000000000..eaef6dc797 --- /dev/null +++ b/modules/entity/spec/sorted_state_adapter.spec.ts @@ -0,0 +1,234 @@ +import { EntityStateAdapter, EntityState } from '../src/models'; +import { createEntityAdapter } from '../src/create_adapter'; +import { + BookModel, + TheGreatGatsby, + AClockworkOrange, + AnimalFarm, +} from './fixtures/book'; + +describe('Sorted State Adapter', () => { + let adapter: EntityStateAdapter; + let state: EntityState; + + beforeEach(() => { + adapter = createEntityAdapter({ + selectId: (book: BookModel) => book.id, + sort: (a, b) => a.title.localeCompare(b.title), + }); + + state = { ids: [], entities: {} }; + }); + + it('should let you add one entity to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + expect(withOneEntity).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + }, + }); + }); + + it('should not change state if you attempt to re-add an entity', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const readded = adapter.addOne(TheGreatGatsby, withOneEntity); + + expect(readded).toEqual(withOneEntity); + }); + + it('should let you add many entities to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withManyMore = adapter.addMany( + [AClockworkOrange, AnimalFarm], + withOneEntity + ); + + expect(withManyMore).toEqual({ + ids: [AClockworkOrange.id, AnimalFarm.id, TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + [AClockworkOrange.id]: AClockworkOrange, + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you add all entities to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withAll = adapter.addAll( + [AClockworkOrange, AnimalFarm], + withOneEntity + ); + + expect(withAll).toEqual({ + ids: [AClockworkOrange.id, AnimalFarm.id], + entities: { + [AClockworkOrange.id]: AClockworkOrange, + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you add remove an entity from the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withoutOne = adapter.removeOne(TheGreatGatsby.id, state); + + expect(withoutOne).toEqual({ + ids: [], + entities: {}, + }); + }); + + it('should let you remove many entities from the state', () => { + const withAll = adapter.addAll( + [TheGreatGatsby, AClockworkOrange, AnimalFarm], + state + ); + + const withoutMany = adapter.removeMany( + [TheGreatGatsby.id, AClockworkOrange.id], + withAll + ); + + expect(withoutMany).toEqual({ + ids: [AnimalFarm.id], + entities: { + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you remove all entities from the state', () => { + const withAll = adapter.addAll( + [TheGreatGatsby, AClockworkOrange, AnimalFarm], + state + ); + + const withoutAll = adapter.removeAll(withAll); + + expect(withoutAll).toEqual({ + ids: [], + entities: {}, + }); + }); + + it('should let you update an entity in the state', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { title: 'A New Hope' }; + + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should not change state if you attempt to update an entity that has not been added', () => { + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes: { title: 'A New Title' }, + }, + state + ); + + expect(withUpdates).toEqual(state); + }); + + it('should let you update the id of entity', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { id: 'A New Id' }; + + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [changes.id], + entities: { + [changes.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should resort correctly if the id and sort key update', () => { + const withOne = adapter.addAll( + [TheGreatGatsby, AnimalFarm, AClockworkOrange], + state + ); + const changes = { id: 'A New Id', title: AnimalFarm.title }; + + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [AClockworkOrange.id, changes.id, AnimalFarm.id], + entities: { + [AClockworkOrange.id]: AClockworkOrange, + [changes.id]: { + ...TheGreatGatsby, + ...changes, + }, + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you update many entities in the state', () => { + const firstChange = { title: 'Zack' }; + const secondChange = { title: 'Aaron' }; + const withMany = adapter.addAll([TheGreatGatsby, AClockworkOrange], state); + + const withUpdates = adapter.updateMany( + [ + { id: TheGreatGatsby.id, changes: firstChange }, + { id: AClockworkOrange.id, changes: secondChange }, + ], + withMany + ); + + expect(withUpdates).toEqual({ + ids: [AClockworkOrange.id, TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...firstChange, + }, + [AClockworkOrange.id]: { + ...AClockworkOrange, + ...secondChange, + }, + }, + }); + }); +}); diff --git a/modules/entity/spec/state_selectors.spec.ts b/modules/entity/spec/state_selectors.spec.ts new file mode 100644 index 0000000000..0e0a9cb55b --- /dev/null +++ b/modules/entity/spec/state_selectors.spec.ts @@ -0,0 +1,57 @@ +import { createEntityAdapter, EntityAdapter, EntityState } from '../src'; +import { EntitySelectors } from '../src/models'; +import { + BookModel, + AClockworkOrange, + AnimalFarm, + TheGreatGatsby, +} from './fixtures/book'; + +describe('Entity State', () => { + interface State { + books: EntityState; + } + + let adapter: EntityAdapter; + let selectors: EntitySelectors; + let state: State; + + beforeEach(() => { + adapter = createEntityAdapter({ + selectId: (book: BookModel) => book.id, + }); + + state = { + books: adapter.addAll( + [AClockworkOrange, AnimalFarm, TheGreatGatsby], + adapter.getInitialState() + ), + }; + + selectors = adapter.getSelectors((state: State) => state.books); + }); + + it('should create a selector for selecting the ids', () => { + const ids = selectors.selectIds(state); + + expect(ids).toEqual(state.books.ids); + }); + + it('should create a selector for selecting the entities', () => { + const entities = selectors.selectEntities(state); + + expect(entities).toEqual(state.books.entities); + }); + + it('should create a selector for selecting the list of models', () => { + const models = selectors.selectAll(state); + + expect(models).toEqual([AClockworkOrange, AnimalFarm, TheGreatGatsby]); + }); + + it('should create a selector for selecting the count of models', () => { + const total = selectors.selectTotal(state); + + expect(total).toEqual(3); + }); +}); diff --git a/modules/entity/spec/unsorted_state_adapter.spec.ts b/modules/entity/spec/unsorted_state_adapter.spec.ts new file mode 100644 index 0000000000..2a9d1176a8 --- /dev/null +++ b/modules/entity/spec/unsorted_state_adapter.spec.ts @@ -0,0 +1,205 @@ +import { EntityStateAdapter, EntityState } from '../src/models'; +import { createEntityAdapter } from '../src/create_adapter'; +import { + BookModel, + TheGreatGatsby, + AClockworkOrange, + AnimalFarm, +} from './fixtures/book'; + +describe('Unsorted State Adapter', () => { + let adapter: EntityStateAdapter; + let state: EntityState; + + beforeEach(() => { + adapter = createEntityAdapter({ + selectId: (book: BookModel) => book.id, + }); + + state = { ids: [], entities: {} }; + }); + + it('should let you add one entity to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + expect(withOneEntity).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + }, + }); + }); + + it('should not change state if you attempt to re-add an entity', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const readded = adapter.addOne(TheGreatGatsby, withOneEntity); + + expect(readded).toEqual(withOneEntity); + }); + + it('should let you add many entities to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withManyMore = adapter.addMany( + [AClockworkOrange, AnimalFarm], + withOneEntity + ); + + expect(withManyMore).toEqual({ + ids: [TheGreatGatsby.id, AClockworkOrange.id, AnimalFarm.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + [AClockworkOrange.id]: AClockworkOrange, + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you add all entities to the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withAll = adapter.addAll( + [AClockworkOrange, AnimalFarm], + withOneEntity + ); + + expect(withAll).toEqual({ + ids: [AClockworkOrange.id, AnimalFarm.id], + entities: { + [AClockworkOrange.id]: AClockworkOrange, + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you add remove an entity from the state', () => { + const withOneEntity = adapter.addOne(TheGreatGatsby, state); + + const withoutOne = adapter.removeOne(TheGreatGatsby.id, state); + + expect(withoutOne).toEqual({ + ids: [], + entities: {}, + }); + }); + + it('should let you remove many entities from the state', () => { + const withAll = adapter.addAll( + [TheGreatGatsby, AClockworkOrange, AnimalFarm], + state + ); + + const withoutMany = adapter.removeMany( + [TheGreatGatsby.id, AClockworkOrange.id], + withAll + ); + + expect(withoutMany).toEqual({ + ids: [AnimalFarm.id], + entities: { + [AnimalFarm.id]: AnimalFarm, + }, + }); + }); + + it('should let you remove all entities from the state', () => { + const withAll = adapter.addAll( + [TheGreatGatsby, AClockworkOrange, AnimalFarm], + state + ); + + const withoutAll = adapter.removeAll(withAll); + + expect(withoutAll).toEqual({ + ids: [], + entities: {}, + }); + }); + + it('should let you update an entity in the state', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { title: 'A New Hope' }; + + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should not change state if you attempt to update an entity that has not been added', () => { + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes: { title: 'A New Title' }, + }, + state + ); + + expect(withUpdates).toEqual(state); + }); + + it('should let you update the id of entity', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { id: 'A New Id' }; + + const withUpdates = adapter.updateOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [changes.id], + entities: { + [changes.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should let you update many entities in the state', () => { + const firstChange = { title: 'First Change' }; + const secondChange = { title: 'Second Change' }; + const withMany = adapter.addAll([TheGreatGatsby, AClockworkOrange], state); + + const withUpdates = adapter.updateMany( + [ + { id: TheGreatGatsby.id, changes: firstChange }, + { id: AClockworkOrange.id, changes: secondChange }, + ], + withMany + ); + + expect(withUpdates).toEqual({ + ids: [TheGreatGatsby.id, AClockworkOrange.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...firstChange, + }, + [AClockworkOrange.id]: { + ...AClockworkOrange, + ...secondChange, + }, + }, + }); + }); +}); diff --git a/modules/entity/src/create_adapter.ts b/modules/entity/src/create_adapter.ts new file mode 100644 index 0000000000..90467c639c --- /dev/null +++ b/modules/entity/src/create_adapter.ts @@ -0,0 +1,30 @@ +import { createSelector } from '@ngrx/store'; +import { + EntityDefinition, + Comparer, + IdSelector, + EntityAdapter, +} from './models'; +import { createInitialStateFactory } from './entity_state'; +import { createSelectorsFactory } from './state_selectors'; +import { createSortedStateAdapter } from './sorted_state_adapter'; +import { createUnsortedStateAdapter } from './unsorted_state_adapter'; + +export function createEntityAdapter(options: { + selectId: IdSelector; + sort?: false | Comparer; +}): EntityAdapter { + const { selectId, sort }: EntityDefinition = { sort: false, ...options }; + + const stateFactory = createInitialStateFactory(); + const selectorsFactory = createSelectorsFactory(); + const stateAdapter = sort + ? createSortedStateAdapter(selectId, sort) + : createUnsortedStateAdapter(selectId); + + return { + ...stateFactory, + ...selectorsFactory, + ...stateAdapter, + }; +} diff --git a/modules/entity/src/entity_state.ts b/modules/entity/src/entity_state.ts new file mode 100644 index 0000000000..aab8653b49 --- /dev/null +++ b/modules/entity/src/entity_state.ts @@ -0,0 +1,20 @@ +import { EntityState } from './models'; + +export function getInitialEntityState(): EntityState { + return { + ids: [], + entities: {}, + }; +} + +export function createInitialStateFactory() { + function getInitialState(): EntityState; + function getInitialState( + additionalState: S + ): EntityState & S; + function getInitialState(additionalState: any = {}): any { + return Object.assign(getInitialEntityState(), additionalState); + } + + return { getInitialState }; +} diff --git a/modules/entity/src/index.ts b/modules/entity/src/index.ts new file mode 100644 index 0000000000..2635a4dbbc --- /dev/null +++ b/modules/entity/src/index.ts @@ -0,0 +1,2 @@ +export { createEntityAdapter } from './create_adapter'; +export { EntityState, EntityAdapter } from './models'; diff --git a/modules/entity/src/models.ts b/modules/entity/src/models.ts new file mode 100644 index 0000000000..a0a4068b64 --- /dev/null +++ b/modules/entity/src/models.ts @@ -0,0 +1,54 @@ +export type Comparer = { + (a: T, b: T): number; +}; + +export type IdSelector = { + (model: T): string; +}; + +export type Dictionary = { + [id: string]: T; +}; + +export type Update = { + id: string; + changes: Partial; +}; + +export interface EntityState { + ids: string[]; + entities: Dictionary; +} + +export interface EntityDefinition { + selectId: IdSelector; + sort: false | Comparer; +} + +export interface EntityStateAdapter { + addOne>(entity: T, state: S): S; + addMany>(entities: T[], state: S): S; + addAll>(entities: T[], state: S): S; + + removeOne>(key: string, state: S): S; + removeMany>(keys: string[], state: S): S; + removeAll>(state: S): S; + + updateOne>(update: Update, state: S): S; + updateMany>(updates: Update[], state: S): S; +} + +export type EntitySelectors = { + selectIds: (state: V) => string[]; + selectEntities: (state: V) => Dictionary; + selectAll: (state: V) => T[]; + selectTotal: (state: V) => number; +}; + +export interface EntityAdapter extends EntityStateAdapter { + getInitialState(): EntityState; + getInitialState(state: S): EntityState & S; + getSelectors( + selectState: (state: V) => EntityState + ): EntitySelectors; +} diff --git a/modules/entity/src/sorted_state_adapter.ts b/modules/entity/src/sorted_state_adapter.ts new file mode 100644 index 0000000000..53bdcca992 --- /dev/null +++ b/modules/entity/src/sorted_state_adapter.ts @@ -0,0 +1,132 @@ +import { + EntityState, + IdSelector, + Comparer, + Dictionary, + EntityStateAdapter, + Update, +} from './models'; +import { createStateOperator } from './state_adapter'; +import { createUnsortedStateAdapter } from './unsorted_state_adapter'; + +export function createSortedStateAdapter( + selectId: IdSelector, + sort: Comparer +): EntityStateAdapter { + type R = EntityState; + + const { removeOne, removeMany, removeAll } = createUnsortedStateAdapter( + selectId + ); + + function addOneMutably(entity: T, state: R): void { + const key = selectId(entity); + const index = state.ids.indexOf(key); + + if (index !== -1) { + return; + } + + const insertAt = findTargetIndex(state, entity); + state.ids.splice(insertAt, 0, key); + state.entities[key] = entity; + } + + function addManyMutably(newModels: T[], state: R): void { + for (let index in newModels) { + addOneMutably(newModels[index], state); + } + } + + function addAllMutably(models: T[], state: R): void { + const sortedModels = models.sort(sort); + + state.entities = {}; + state.ids = sortedModels.map(model => { + const id = selectId(model); + state.entities[id] = model; + return id; + }); + } + + function updateOneMutably(update: Update, state: R): void { + const index = state.ids.indexOf(update.id); + + if (index === -1) { + return; + } + + const original = state.entities[update.id]; + const updated: T = Object.assign({}, original, update.changes); + const updatedKey = selectId(updated); + const result = sort(original, updated); + + if (result === 0) { + if (updatedKey !== update.id) { + delete state.entities[update.id]; + state.ids[index] = updatedKey; + } + + state.entities[updatedKey] = updated; + + return; + } + + state.ids.splice(index, 1); + state.ids.splice(findTargetIndex(state, updated), 0, updatedKey); + + if (updatedKey !== update.id) { + delete state.entities[update.id]; + } + + state.entities[updatedKey] = updated; + } + + function updateManyMutably(updates: Update[], state: R): void { + for (let index in updates) { + updateOneMutably(updates[index], state); + } + } + + function findTargetIndex( + state: R, + model: T, + left = 0, + right = state.ids.length - 1 + ) { + if (right === -1) { + return 0; + } + + let middle: number; + + while (true) { + middle = Math.floor((left + right) / 2); + + const result = sort(state.entities[state.ids[middle]], model); + + if (result === 0) { + return middle; + } else if (result < 0) { + left = middle + 1; + } else { + right = middle - 1; + } + + if (left > right) { + return state.ids.length - 1; + } + } + } + + return { + removeOne, + removeMany, + removeAll, + addOne: createStateOperator(addOneMutably), + updateOne: createStateOperator(updateOneMutably), + addAll: createStateOperator(addAllMutably), + addMany: createStateOperator(addManyMutably), + updateMany: createStateOperator(updateManyMutably), + }; +} diff --git a/modules/entity/src/state_adapter.ts b/modules/entity/src/state_adapter.ts new file mode 100644 index 0000000000..6a6b24f744 --- /dev/null +++ b/modules/entity/src/state_adapter.ts @@ -0,0 +1,16 @@ +import { EntityState, EntityStateAdapter } from './models'; + +export function createStateOperator( + mutator: (arg: R, state: EntityState) => void +) { + return function operation>(arg: R, state: S): S { + const clonedEntityState: EntityState = { + ids: [...state.ids], + entities: { ...state.entities }, + }; + + mutator(arg, clonedEntityState); + + return Object.assign({}, state, clonedEntityState); + }; +} diff --git a/modules/entity/src/state_selectors.ts b/modules/entity/src/state_selectors.ts new file mode 100644 index 0000000000..260edfb91e --- /dev/null +++ b/modules/entity/src/state_selectors.ts @@ -0,0 +1,27 @@ +import { createSelector } from '@ngrx/store'; +import { EntityState, EntitySelectors } from './models'; + +export function createSelectorsFactory() { + return { + getSelectors( + selectState: (state: V) => EntityState + ): EntitySelectors { + const selectIds = (state: EntityState) => state.ids; + const selectEntities = (state: EntityState) => state.entities; + const selectAll = createSelector( + selectIds, + selectEntities, + (ids, entities) => ids.map(id => entities[id]) + ); + + const selectTotal = createSelector(selectIds, ids => ids.length); + + return { + selectIds: createSelector(selectState, selectIds), + selectEntities: createSelector(selectState, selectEntities), + selectAll: createSelector(selectState, selectAll), + selectTotal: createSelector(selectState, selectTotal), + }; + }, + }; +} diff --git a/modules/entity/src/unsorted_state_adapter.ts b/modules/entity/src/unsorted_state_adapter.ts new file mode 100644 index 0000000000..afc6980055 --- /dev/null +++ b/modules/entity/src/unsorted_state_adapter.ts @@ -0,0 +1,93 @@ +import { EntityState, EntityStateAdapter, IdSelector, Update } from './models'; +import { createStateOperator } from './state_adapter'; + +export function createUnsortedStateAdapter( + selectId: IdSelector +): EntityStateAdapter { + type R = EntityState; + + function addOneMutably(entity: T, state: R): void { + const key = selectId(entity); + const index = state.ids.indexOf(key); + + if (index !== -1) { + return; + } + + state.ids.push(key); + state.entities[key] = entity; + } + + function addManyMutably(entities: T[], state: R): void { + for (let index in entities) { + addOneMutably(entities[index], state); + } + } + + function addAllMutably(entities: T[], state: R): void { + state.ids = []; + state.entities = {}; + + addManyMutably(entities, state); + } + + function removeOneMutably(key: string, state: R): void { + const index = state.ids.indexOf(key); + + if (index === -1) { + return; + } + + state.ids.splice(index, 1); + delete state.entities[key]; + } + + function removeManyMutably(keys: string[], state: R): void { + for (let index in keys) { + removeOneMutably(keys[index], state); + } + } + + function removeAll(state: S): S { + return Object.assign({}, state, { + ids: [], + entities: {}, + }); + } + + function updateOneMutably(update: Update, state: R): void { + const index = state.ids.indexOf(update.id); + + if (index === -1) { + return; + } + + const original = state.entities[update.id]; + const updated: T = Object.assign({}, original, update.changes); + const newKey = selectId(updated); + + if (newKey !== update.id) { + state.ids[index] = newKey; + delete state.entities[update.id]; + } + + state.entities[newKey] = updated; + } + + function updateManyMutably(updates: Update[], state: R): void { + for (let index in updates) { + updateOneMutably(updates[index], state); + } + } + + return { + removeAll, + addOne: createStateOperator(addOneMutably), + addMany: createStateOperator(addManyMutably), + addAll: createStateOperator(addAllMutably), + updateOne: createStateOperator(updateOneMutably), + updateMany: createStateOperator(updateManyMutably), + removeOne: createStateOperator(removeOneMutably), + removeMany: createStateOperator(removeManyMutably), + }; +} diff --git a/modules/entity/tsconfig-build.json b/modules/entity/tsconfig-build.json new file mode 100644 index 0000000000..bdba0a3200 --- /dev/null +++ b/modules/entity/tsconfig-build.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "stripInternal": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "es2015", + "moduleResolution": "node", + "noEmitOnError": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "outDir": "../../dist/packages/entity", + "paths": { + "@ngrx/store": ["../../dist/packages/store"] + }, + "rootDir": ".", + "sourceMap": true, + "inlineSources": true, + "lib": ["es2015", "dom"], + "target": "es2015", + "skipLibCheck": true + }, + "files": [ + "public_api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@ngrx/entity" + } +} \ No newline at end of file diff --git a/package.json b/package.json index d599b95b2f..967636461e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "core-js": "^2.4.1", "coveralls": "^2.13.0", "cpy-cli": "^1.0.1", + "deep-freeze": "^0.0.1", "fs-extra": "^2.1.2", "glob": "^7.1.1", "hammerjs": "^2.0.8", diff --git a/yarn.lock b/yarn.lock index eb745e27aa..123cdf2402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -194,18 +194,14 @@ version "2.0.29" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" -"@types/node@*": - version "8.0.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.10.tgz#12efec9183b072d5f951cf86395a4c780f868a17" +"@types/node@*", "@types/node@^7.0.5": + version "7.0.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.34.tgz#eed5c95291a9dddff6b9f5a72ca342b1e72f0ba2" "@types/node@^6.0.46": version "6.0.68" resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.68.tgz#0c43b6b8b9445feb86a0fbd3457e3f4bc591e66d" -"@types/node@^7.0.5": - version "7.0.34" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.34.tgz#eed5c95291a9dddff6b9f5a72ca342b1e72f0ba2" - "@types/ora@^0.3.31": version "0.3.31" resolved "https://registry.yarnpkg.com/@types/ora/-/ora-0.3.31.tgz#1a4bf16bd62ec2764b8f40b0e2f4d85c21292f83" @@ -595,14 +591,14 @@ babel-types@^6.18.0, babel-types@^6.23.0: lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: - version "6.16.1" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" - -babylon@^6.17.4: +babylon@^6.11.0, babylon@^6.15.0, babylon@^6.17.4: version "6.17.4" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" +babylon@^6.13.0: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -1337,22 +1333,7 @@ conventional-changelog-writer@^1.1.0: split "^1.0.0" through2 "^2.0.0" -conventional-changelog@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-1.1.4.tgz#108bc750c2a317e200e2f9b413caaa1f8c7efa3b" - dependencies: - conventional-changelog-angular "^1.3.4" - conventional-changelog-atom "^0.1.0" - conventional-changelog-codemirror "^0.1.0" - conventional-changelog-core "^1.9.0" - conventional-changelog-ember "^0.2.6" - conventional-changelog-eslint "^0.1.0" - conventional-changelog-express "^0.1.0" - conventional-changelog-jquery "^0.1.0" - conventional-changelog-jscs "^0.1.0" - conventional-changelog-jshint "^0.1.0" - -conventional-changelog@^1.1.4: +conventional-changelog@^1.1.3, conventional-changelog@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-1.1.4.tgz#108bc750c2a317e200e2f9b413caaa1f8c7efa3b" dependencies: @@ -1741,6 +1722,10 @@ deep-freeze-strict@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" +deep-freeze@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -2885,14 +2870,10 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -iconv-lite@0.4.15: +iconv-lite@0.4.15, iconv-lite@~0.4.13: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" -iconv-lite@~0.4.13: - version "0.4.18" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -3250,14 +3231,14 @@ istanbul-instrumenter-loader@^2.0.0: loader-utils "^0.2.16" object-assign "^4.1.0" -istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0, istanbul-lib-coverage@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212" - -istanbul-lib-coverage@^1.1.1: +istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0, istanbul-lib-coverage@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" +istanbul-lib-coverage@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212" + istanbul-lib-hook@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.4.tgz#1919debbc195807880041971caf9c7e2be2144d6" @@ -4944,14 +4925,10 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -q@1.4.1: +q@1.4.1, q@^1.1.2, q@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -q@^1.1.2, q@^1.4.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" - qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" @@ -6043,7 +6020,7 @@ tmp@0.0.24: version "0.0.24" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.24.tgz#d6a5e198d14a9835cc6f2d7c3d9e302428c8cf12" -tmp@0.0.28: +tmp@0.0.28, tmp@0.0.x: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" dependencies: @@ -6055,7 +6032,7 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.0.x, tmp@^0.0.31: +tmp@^0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: @@ -6345,14 +6322,10 @@ uuid@^2.0.1, uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0: +uuid@^3.0.0, uuid@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" -uuid@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - v8flags@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881" @@ -6622,20 +6595,13 @@ write-pkg@^3.0.1: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@1.1.1: +ws@1.1.1, ws@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" dependencies: options ">=0.0.5" ultron "1.0.x" -ws@^1.0.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.4.tgz#57f40d036832e5f5055662a397c4de76ed66bf61" - dependencies: - options ">=0.0.5" - ultron "1.0.x" - wtf-8@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" From 1df963cfbbdea0ba5fd6b916aa8877f0d30385a9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 31 Jul 2017 20:29:27 -0500 Subject: [PATCH 03/67] chore(docs): Added migration docs for router-store and testing store --- MIGRATION.md | 115 +++++++++++++++++- docs/store/README.md | 12 +- docs/store/api.md | 29 +++++ docs/store/testing.md | 110 +++++++++++++++++ .../app/books/containers/collection-page.ts | 11 +- example-app/app/books/effects/collection.ts | 6 - 6 files changed, 265 insertions(+), 18 deletions(-) create mode 100644 docs/store/testing.md diff --git a/MIGRATION.md b/MIGRATION.md index 6a1b686a67..90e89378d6 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -17,6 +17,13 @@ The sections below cover the changes between the ngrx projects migrating from V1 [@ngrx/router-store](#ngrxrouter-store) [@ngrx/store-devtools](#ngrxstore-devtools) +## Dependencies + +You need to have the latest versions of TypeScript and RxJS to use ngrx V4 libraries. + +TypeScript 2.4.x +RxJS 5.4.x + ## @ngrx/core @ngrx/core is no longer needed, and can conflict with @ngrx/store. You should remove it from your project. @@ -215,7 +222,7 @@ import * as auth from '../actions/auth.actions'; @Injectable() export class AppEffects { - + @Effect() init$: Observable = this.actions$ .ofType(Dispatcher.INIT) @@ -224,7 +231,7 @@ export class AppEffects { return of(new auth.LoginAction()); }); - + constructor(private actions$: Actions) { } } ``` @@ -242,14 +249,14 @@ import * as auth from '../actions/auth.actions'; @Injectable() export class AppEffects { - + @Effect() init$: Observable = defer(() => { return of(new auth.LoginAction()); }); - + constructor(private actions$: Actions) { } } @@ -336,6 +343,8 @@ describe('My Effects', () => { ## @ngrx/router-store +### Registering the module + BEFORE: `reducers/index.ts` @@ -407,6 +416,104 @@ import { reducers } from './reducers'; export class AppModule {} ``` +### Navigation actions + +Navigation actions are not provided as part of the V4 package. You provide your own +custom navigation actions that use the `Router` within effects to navigate. + +BEFORE: + +```ts +import { go } from '@ngrx/router-store'; + +store.dispatch(go(['/path', { routeParam: 1 }], { page: 1 }, { replaceUrl: false })); + +store.dispatch(back()); + +store.dispatch(forward()); +``` + +AFTER: + +```ts +import { Action } from '@ngrx/store'; +import { NavigationExtras } from '@angular/router'; + +export const GO = '[Router] Go'; +export const BACK = '[Router] Back'; +export const FORWARD = '[Router] Forward'; + +export class Go implements Action { + readonly type = GO; + + constructor(public payload: { + path: any[]; + query?: object; + extras?: NavigationExtras; + }) {} +} + +export class Back implements Action { + readonly type = BACK; +} + +export class Forward implements Action { + readonly type = FOWARD; +} + +export type Actions + = Go + | Back + | Forward; +``` + +```ts +import * as RouterActions from './actions/router'; + +store.dispatch(new RouterActions.Go({ + path: ['/path', { routeParam: 1 }], + query: { page: 1 }, + extras: { replaceUrl: false } +}); + +store.dispatch(new RouterActions.Back()); + +store.dispatch(new RouterActions.Forward()); +``` + +```ts +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/map'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { Location } from '@angular/common'; +import { Effect, Actions } from '@ngrx/effects'; +import * as RouterActions from './actions/router'; + +@Injectable() +export class RouterEffects { + @Effect({ dispatch: false }) + navigate$ = this.actions$.ofType(RouterActions.GO) + .map((action: RouterActions.Go) => action.payload) + .do(({ path, query: queryParams, extras}) + => this.router.navigate(path, { queryParams, ...extras })); + + @Effect({ dispatch: false }) + navigateBack$ = this.actions$.ofType(RouterActions.BACK) + .do(() => this.location.back()); + + @Effect({ dispatch: false }) + navigateForward$ = this.actions$.ofType(RouterActions.FORWARD) + .do(() => this.location.forward()); + + constructor( + private actions$: Actions, + private router: Router, + private location: Location + ) {} +} +``` + ## @ngrx/store-devtools **NOTE:** store-devtools currently causes severe performance problems when diff --git a/docs/store/README.md b/docs/store/README.md index 235ac9bd19..61f7b5173d 100644 --- a/docs/store/README.md +++ b/docs/store/README.md @@ -115,12 +115,14 @@ export class MyAppComponent { ## API Documentation - [Action Reducers](./actions.md#action-reducers) -- [Typed Actions](./actions.md#typed-actions) -- [Selectors](./selectors.md) -- [State composition through feature modules](./api.md#feature-module-state-composition) -- [Providing initial state](./api.md#initial-state) -- [Meta-Reducers](./api.md#meta-reducers) - [Injecting reducers](./api.md#injecting-reducers) +- [Meta-Reducers/Enhancers](./api.md#meta-reducers) +- [Providing initial state](./api.md#initial-state) +- [State composition through feature modules](./api.md#feature-module-state-composition) +- [State selectors](./selectors.md) +- [Testing](./testing.md) +- [Typed Actions](./actions.md#typed-actions) + ### Additional Material - [From Inactive to Reactive with ngrx](https://www.youtube.com/watch?v=cyaAhXHhxgk) diff --git a/docs/store/api.md b/docs/store/api.md index d4afba1fe5..4fee0303ba 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -123,3 +123,32 @@ export function getReducers(someService: SomeService) { }) export class AppModule { } ``` + +Reducers are also injected when composing state through feature modules. + +```ts +import { NgModule, InjectionToken } from '@angular/core'; +import { StoreModule, ActionReducerMap } from '@ngrx/store'; + +import * as fromFeature from './reducers'; + +export const FEATURE_REDUCER_TOKEN = new InjectionToken>('Feature Reducers'); + +// map of reducers +export const reducers: ActionReducerMap = { + +}; + +@NgModule({ + imports: [ + StoreModule.forFeature('feature', FEATURE_REDUCER_TOKEN), + ], + providers: [ + { + provide: FEATURE_REDUCER_TOKEN, + useValue: reducers + } + ] +}) +export class FeatureModule { } +``` diff --git a/docs/store/testing.md b/docs/store/testing.md new file mode 100644 index 0000000000..746251b924 --- /dev/null +++ b/docs/store/testing.md @@ -0,0 +1,110 @@ +# Testing + +### Providing Store for testing +Use the `StoreModule.forRoot` in your `TestBed` configuration when testing components or services that inject `Store`. + +* Reducing state is synchronous, so mocking out the `Store` isn't required. +* Use the `combineReducers` method with the map of feature reducers to compose the `State` for the test. +* Dispatch actions to load data into the `Store`. + +my-component.ts +```ts +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import * as fromFeature from '../reducers'; +import * as Data from '../actions/data'; + +@Component({ + selector: 'my-component', + template: ` +
{{ item }}
+ + + `, +}) +export class MyComponent implements OnInit { + items$ = this.store.select(fromFeature.selectFeatureItems); + + constructor(private store: Store) {} + + ngOnInit() { + this.store.dispatch(new Data.LoadData()); + } + + onRefresh() { + this.store.dispatch(new Data.RefreshItems()); + } +} +``` + +my-component.spec.ts +```ts +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import { MyComponent } from './my.component'; +import * as fromRoot from '../reducers'; +import * as fromFeature from './reducers'; +import * as Data from '../actions/data'; + +describe('My Component', () => { + let component: MyComponent; + let fixture: ComponentFixture; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ + ...fromRoot.reducers, + 'feature': combineReducers(fromFeature.reducers) + }), + // other imports + ], + declarations: [ + MyComponent, + // other declarations + ], + providers: [ + // other providers + ] + }); + + store = TestBed.get(Store); + + spyOn(store.dispatch).and.callThrough(); + + fixture = TestBed.createComponent(MyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should dispatch an action to load data when created', () => { + const action = new Data.LoadData(); + + expect(store.dispatch).toHaveBeenCalledWith(action); + }); + + it('should dispatch an action to refreshing data', () => { + const action = new Data.RefreshData(); + + component.onRefresh(); + + expect(store.dispatch).toHaveBeenCalledWith(action); + }); + + it('should display a list of items after the data is loaded', () => { + const items = [1, 2, 3]; + const action = new Data.LoadDataSuccess({ items }); + + store.dispatch(action); + + component.items$.subscribe(data => { + expect(data.length).toBe(items.length); + }); + }); +}); +``` diff --git a/example-app/app/books/containers/collection-page.ts b/example-app/app/books/containers/collection-page.ts index 841566b7ea..89876c77f7 100644 --- a/example-app/app/books/containers/collection-page.ts +++ b/example-app/app/books/containers/collection-page.ts @@ -1,9 +1,10 @@ import 'rxjs/add/operator/let'; -import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import * as fromBooks from '../reducers'; +import * as collection from '../actions/collection'; import { Book } from '../models/book'; @Component({ @@ -31,10 +32,14 @@ import { Book } from '../models/book'; `, ], }) -export class CollectionPageComponent { +export class CollectionPageComponent implements OnInit { books$: Observable; - constructor(store: Store) { + constructor(private store: Store) { this.books$ = store.select(fromBooks.getBookCollection); } + + ngOnInit() { + this.store.dispatch(new collection.LoadAction()); + } } diff --git a/example-app/app/books/effects/collection.ts b/example-app/app/books/effects/collection.ts index 7182681c51..d14ecebba3 100644 --- a/example-app/app/books/effects/collection.ts +++ b/example-app/app/books/effects/collection.ts @@ -1,6 +1,5 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/toArray'; @@ -32,14 +31,9 @@ export class CollectionEffects { return this.db.open('books_app'); }); - /** - * This effect makes use of the `startWith` operator to trigger - * the effect immediately on startup. - */ @Effect() loadCollection$: Observable = this.actions$ .ofType(collection.LOAD) - .startWith(new collection.LoadAction()) .switchMap(() => this.db .query('books') From c79853b22ec469675be3d83de39587ecfe75cb1e Mon Sep 17 00:00:00 2001 From: Juri Strumpflohner Date: Tue, 1 Aug 2017 19:08:25 +0200 Subject: [PATCH 04/67] build: fix type def files search path (#225) This commit fixes the search path for type definition files of the build task which otherwise occasionally results in a build error (reported on Windows and OSX). --- build/tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/tasks.ts b/build/tasks.ts index 927e2ca709..c1b97a09c9 100644 --- a/build/tasks.ts +++ b/build/tasks.ts @@ -158,7 +158,7 @@ export async function removeRemainingSourceMapFiles(config: Config) { export async function copyTypeDefinitionFiles(config: Config) { const packages = util.getTopLevelPackages(config); const files = await util.getListOfFiles( - `./dist/packages/?(${packages.join('|')})/**/*` + `./dist/packages/?(${packages.join('|')})/**/*.*` ); await mapAsync(files, async file => { From e2f1e5743752f2a7496d8f7ccecd82d259f5593b Mon Sep 17 00:00:00 2001 From: 03byron Date: Wed, 2 Aug 2017 15:55:51 +0200 Subject: [PATCH 05/67] fix(createSelector): memoize projector function (#228) Closes #226 --- modules/store/spec/selector.spec.ts | 17 +++++++++++++++++ modules/store/src/selector.ts | 13 +++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index d2db3ddb8c..ac563fdabe 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -41,6 +41,23 @@ describe('Selectors', () => { expect(projectFn).toHaveBeenCalledWith(countOne, countTwo); }); + it('should call the projector function only when the value of a dependent selector change', () => { + const firstState = { first: 'state', unchanged: 'state' }; + const secondState = { second: 'state', unchanged: 'state' }; + const neverChangingSelector = jasmine + .createSpy('unchangedSelector') + .and.callFake((state: any) => { + return state.unchanged; + }); + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector(neverChangingSelector, projectFn); + + selector(firstState); + selector(secondState); + + expect(projectFn).toHaveBeenCalledTimes(1); + }); + it('should memoize the function', () => { const firstState = { first: 'state' }; const secondState = { second: 'state' }; diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index 00c34f84c9..2a20df84ad 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -115,19 +115,24 @@ export function createSelector(...args: any[]): Selector { selector.release && typeof selector.release === 'function' ); - const { memoized, reset } = memoize(function(state: any) { + const memoizedProjector = memoize(function(...selectors: any[]) { + return projector.apply(null, selectors); + }); + + const memoizedState = memoize(function(state: any) { const args = selectors.map(fn => fn(state)); - return projector.apply(null, args); + return memoizedProjector.memoized.apply(null, args); }); function release() { - reset(); + memoizedState.reset(); + memoizedProjector.reset(); memoizedSelectors.forEach(selector => selector.release()); } - return Object.assign(memoized, { release }); + return Object.assign(memoizedState.memoized, { release }); } export function createFeatureSelector( From 2c006e8fac04225d0694c4e08f2ad42b5822ee02 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 2 Aug 2017 08:56:44 -0500 Subject: [PATCH 06/67] fix(RouterStore): Add support for cancellation with CanLoad guard (#223) Closes #213 --- modules/router-store/spec/integration.spec.ts | 41 ++++++++++++++++++- .../router-store/src/router_store_module.ts | 7 +++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index a43e8e419a..4a16632a26 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -286,10 +286,40 @@ describe('integration spec', () => { done(); }); }); + + it('should support cancellation of initial navigation using canLoad guard', done => { + const reducer = (state: any, action: RouterAction) => { + const r = routerReducer(state, action); + return r && r.state + ? { url: r.state.url, navigationId: r.navigationId } + : null; + }; + + createTestModule({ + reducers: { routerReducer, reducer }, + canLoad: () => false, + }); + + const router = TestBed.get(Router); + const store = TestBed.get(Store); + const log = logOfRouterAndStore(router, store); + + router.navigateByUrl('/load').then((r: boolean) => { + expect(r).toBe(false); + + expect(log).toEqual([ + { type: 'store', state: null }, + { type: 'router', event: 'NavigationStart', url: '/load' }, + { type: 'store', state: null }, + { type: 'router', event: 'NavigationCancel', url: '/load' }, + ]); + done(); + }); + }); }); function createTestModule( - opts: { reducers?: any; canActivate?: Function } = {} + opts: { reducers?: any; canActivate?: Function; canLoad?: Function } = {} ) { @Component({ selector: 'test-app', @@ -314,6 +344,11 @@ function createTestModule( component: SimpleCmp, canActivate: ['CanActivateNext'], }, + { + path: 'load', + loadChildren: 'test', + canLoad: ['CanLoadNext'], + }, ]), StoreRouterConnectingModule, ], @@ -322,6 +357,10 @@ function createTestModule( provide: 'CanActivateNext', useValue: opts.canActivate || (() => true), }, + { + provide: 'CanLoadNext', + useValue: opts.canLoad || (() => true), + }, ], }); diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 4f331e9bac..1ee0481841 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -185,7 +185,12 @@ export class StoreRouterConnectingModule { } private navigateIfNeeded(): void { - if (!this.storeState['routerReducer']) return; + if ( + !this.storeState['routerReducer'] || + !this.storeState['routerReducer'].state + ) { + return; + } if (this.dispatchTriggeredByRouter) return; if (this.router.url !== this.storeState['routerReducer'].state.url) { From 98c89c4654fb168ba5910e2dc7a8df49e64c3638 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Wed, 2 Aug 2017 08:59:21 -0500 Subject: [PATCH 07/67] v4.0.2 --- lerna.json | 2 +- modules/effects/package.json | 2 +- modules/entity/package.json | 2 +- modules/router-store/package.json | 2 +- modules/store/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lerna.json b/lerna.json index a31a272302..85a01f3e10 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "modules/*" ], - "version": "4.0.1", + "version": "4.0.2", "npmClient": "yarn" } diff --git a/modules/effects/package.json b/modules/effects/package.json index ff8b0dabe0..dd3c28f0ec 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/effects", - "version": "4.0.1", + "version": "4.0.2", "description": "Side effect model for @ngrx/store", "module": "@ngrx/effects.es5.js", "es2015": "@ngrx/effects.js", diff --git a/modules/entity/package.json b/modules/entity/package.json index 791b17138f..8a76a1c382 100644 --- a/modules/entity/package.json +++ b/modules/entity/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/entity", - "version": "4.0.1", + "version": "4.0.2", "description": "Common utilities for entity reducers", "module": "@ngrx/entity.es5.js", "es2015": "@ngrx/entity.js", diff --git a/modules/router-store/package.json b/modules/router-store/package.json index ae9839248a..7869445073 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/router-store", - "version": "4.0.0", + "version": "4.0.2", "description": "Bindings to connect @angular/router to @ngrx/store", "module": "@ngrx/router-store.es5.js", "es2015": "@ngrx/router-store.js", diff --git a/modules/store/package.json b/modules/store/package.json index caaf57cf59..a71b05f592 100644 --- a/modules/store/package.json +++ b/modules/store/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/store", - "version": "4.0.0", + "version": "4.0.2", "description": "RxJS powered Redux for Angular apps", "module": "@ngrx/store.es5.js", "es2015": "@ngrx/store.js", From f969676c56b29bd585bf263ce4131d1b4c84d25d Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Wed, 2 Aug 2017 09:01:53 -0500 Subject: [PATCH 08/67] docs(CHANGELOG): Update Changelog for 4.0.2 --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b3422917..b61b472a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,24 @@ -# [4.0.0](https://github.com/ngrx/platform/compare/v4.0.1...v4.0.0) (2017-07-22) +# [4.0.0](https://github.com/ngrx/platform/compare/v4.0.2...v4.0.0) (2017-08-02) + + + + +## [4.0.2](https://github.com/ngrx/platform/compare/v4.0.1...v4.0.2) (2017-08-02) ### Bug Fixes -* **docs:** update angular-cli variable ([eeb7d5d](https://github.com/ngrx/platform/commit/eeb7d5d)) +* **createSelector:** memoize projector function (#228) ([e2f1e57](https://github.com/ngrx/platform/commit/e2f1e57)), closes [#226](https://github.com/ngrx/platform/issues/226) * **Effects:** Wrap testing source in an Actions observable (#121) ([bfdb83b](https://github.com/ngrx/platform/commit/bfdb83b)), closes [#117](https://github.com/ngrx/platform/issues/117) +* **RouterStore:** Add support for cancellation with CanLoad guard (#223) ([2c006e8](https://github.com/ngrx/platform/commit/2c006e8)), closes [#213](https://github.com/ngrx/platform/issues/213) * **Store:** Remove auto-memoization of selector functions ([90899f7](https://github.com/ngrx/platform/commit/90899f7)), closes [#118](https://github.com/ngrx/platform/issues/118) ### Features * **Effects:** Add generic type to the "ofType" operator ([55c13b2](https://github.com/ngrx/platform/commit/55c13b2)) +* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) * **Store:** Added initial state function support for features. Added more tests (#85) ([5e5d7dd](https://github.com/ngrx/platform/commit/5e5d7dd)) From 065d33ef6f86a64f044470f4ba8d135ed8fe2ef6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 3 Aug 2017 09:00:08 -0500 Subject: [PATCH 09/67] fix(Effects): Ensure StoreModule is loaded before effects (#230) Closes #184, #219 --- modules/effects/src/effects_feature_module.ts | 6 ++++-- modules/effects/src/effects_root_module.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/effects/src/effects_feature_module.ts b/modules/effects/src/effects_feature_module.ts index 80e947ef83..77cc6ae2ea 100644 --- a/modules/effects/src/effects_feature_module.ts +++ b/modules/effects/src/effects_feature_module.ts @@ -1,4 +1,5 @@ -import { NgModule, Inject, Type } from '@angular/core'; +import { NgModule, Inject, Optional } from '@angular/core'; +import { StoreModule } from '@ngrx/store'; import { EffectsRootModule } from './effects_root_module'; import { FEATURE_EFFECTS } from './tokens'; @@ -6,7 +7,8 @@ import { FEATURE_EFFECTS } from './tokens'; export class EffectsFeatureModule { constructor( private root: EffectsRootModule, - @Inject(FEATURE_EFFECTS) effectSourceGroups: any[][] + @Inject(FEATURE_EFFECTS) effectSourceGroups: any[][], + @Optional() storeModule: StoreModule ) { effectSourceGroups.forEach(group => group.forEach(effectSourceInstance => diff --git a/modules/effects/src/effects_root_module.ts b/modules/effects/src/effects_root_module.ts index 62f4ddb959..279d985b70 100644 --- a/modules/effects/src/effects_root_module.ts +++ b/modules/effects/src/effects_root_module.ts @@ -1,4 +1,5 @@ -import { NgModule, Inject } from '@angular/core'; +import { NgModule, Inject, Optional } from '@angular/core'; +import { StoreModule } from '@ngrx/store'; import { EffectsRunner } from './effects_runner'; import { EffectSources } from './effect_sources'; import { ROOT_EFFECTS } from './tokens'; @@ -8,7 +9,8 @@ export class EffectsRootModule { constructor( private sources: EffectSources, runner: EffectsRunner, - @Inject(ROOT_EFFECTS) rootEffects: any[] + @Inject(ROOT_EFFECTS) rootEffects: any[], + @Optional() storeModule: StoreModule ) { runner.start(); From 2b1a076b1d6308acf7ec96cef6a506574834adc6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 3 Aug 2017 09:00:49 -0500 Subject: [PATCH 10/67] fix(Effects): Export EffectsNotification interface (#231) --- docs/effects/api.md | 4 ++-- modules/effects/src/index.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/effects/api.md b/docs/effects/api.md index d27d7bd8e2..ffdbf151a0 100644 --- a/docs/effects/api.md +++ b/docs/effects/api.md @@ -107,7 +107,7 @@ import 'rxjs/add/operator/takeUntil'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Action } from '@ngrx/store'; -import { Actions, Effect, OnRunEffects } from '@ngrx/effects'; +import { Actions, Effect, OnRunEffects, EffectsNotification } from '@ngrx/effects'; @Injectable() export class UserEffects implements OnRunEffects { @@ -119,7 +119,7 @@ export class UserEffects implements OnRunEffects { console.log(action); }); - ngrxOnRunEffects(resolvedEffects$: Observable) { + ngrxOnRunEffects(resolvedEffects$: Observable) { return this.actions$.ofType('LOGGED_IN') .exhaustMap(() => resolvedEffects$.takeUntil('LOGGED_OUT')); } diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index c314508b9a..cd112564fb 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -5,3 +5,4 @@ export { EffectsModule } from './effects_module'; export { EffectSources } from './effect_sources'; export { OnRunEffects } from './on_run_effects'; export { toPayload } from './util'; +export { EffectNotification } from './effect_notification'; From 4aec80c7f750cc325e9f898f1152618d78971b68 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 4 Aug 2017 09:15:18 -0500 Subject: [PATCH 11/67] fix(Store): Set initial state for feature modules (#235) Closes #206, #233 --- modules/store/spec/integration.spec.ts | 45 ++++++++++++++++++++++++++ modules/store/src/reducer_manager.ts | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/modules/store/spec/integration.spec.ts b/modules/store/spec/integration.spec.ts index 16694a80dd..590c748e79 100644 --- a/modules/store/spec/integration.spec.ts +++ b/modules/store/spec/integration.spec.ts @@ -181,4 +181,49 @@ describe('ngRx Integration spec', () => { expect(currentlyVisibleTodos.length).toBe(0); }); }); + + describe('feature state', () => { + const initialState = { + todos: [ + { + id: 1, + text: 'do things', + completed: false, + }, + ], + visibilityFilter: VisibilityFilters.SHOW_ALL, + }; + + const reducers: ActionReducerMap = { + todos: todos, + visibilityFilter: visibilityFilter, + }; + + const featureInitialState = [{ id: 1, completed: false, text: 'Item' }]; + + it('should initialize properly', () => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot(reducers, { initialState }), + StoreModule.forFeature('items', todos, { + initialState: featureInitialState, + }), + ], + }); + + const store: Store = TestBed.get(Store); + + let expected = [ + { + todos: initialState.todos, + visibilityFilter: initialState.visibilityFilter, + items: featureInitialState, + }, + ]; + + store.select(state => state).subscribe(state => { + expect(state).toEqual(expected.shift()); + }); + }); + }); }); diff --git a/modules/store/src/reducer_manager.ts b/modules/store/src/reducer_manager.ts index a3f0ee58f8..16054875a1 100644 --- a/modules/store/src/reducer_manager.ts +++ b/modules/store/src/reducer_manager.ts @@ -40,7 +40,7 @@ export class ReducerManager extends BehaviorSubject> }: StoreFeature) { const reducer = typeof reducers === 'function' - ? reducers + ? (state = initialState, action: any) => reducers(state, action) : createReducerFactory(reducerFactory, metaReducers)( reducers, initialState From 7d23fdbe332ee7ece08ef5a283f93d04020c9412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 5 Aug 2017 20:54:13 -0500 Subject: [PATCH 12/67] chore(docs): Update store README text (#238) --- docs/store/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/store/README.md b/docs/store/README.md index 61f7b5173d..b3bd1ad5f0 100644 --- a/docs/store/README.md +++ b/docs/store/README.md @@ -54,7 +54,7 @@ export function counterReducer(state: number = 0, action: Action) { ``` To register the state container within your application, import the reducers and use the `StoreModule.forRoot` -function in the `imports` array them in the `AppModule`. +function in the `imports` array of the `@NgModule` decorator for your `AppModule`. ```ts import { NgModule } from '@angular/core' From 84390a19c828cb890dd7b5dd59b5865063b7b706 Mon Sep 17 00:00:00 2001 From: Sharikov Vladislav Date: Tue, 8 Aug 2017 02:48:40 +0300 Subject: [PATCH 13/67] chore(Example): Fix E2E test setup (#242) --- e2e/app.e2e-spec.ts | 4 ++-- e2e/app.po.ts | 4 ++-- protractor.conf.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index 479a59c5ab..7dddcd57ab 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -7,8 +7,8 @@ describe('example-app App', function() { page = new ExampleAppPage(); }); - it('should display message saying app works', () => { + it('should display the app title in the menu', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('app works!'); + expect(page.getAppDescription()).toContain('Book Collection'); }); }); diff --git a/e2e/app.po.ts b/e2e/app.po.ts index 649759091d..af4d72e7be 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -5,7 +5,7 @@ export class ExampleAppPage { return browser.get('/'); } - getParagraphText() { - return element(by.css('app-root h1')).getText(); + getAppDescription() { + return element(by.css('md-toolbar-row')).getText(); } } diff --git a/protractor.conf.js b/protractor.conf.js index c819669c0a..2cbc329391 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -22,7 +22,7 @@ exports.config = { }, beforeLaunch: function() { require('ts-node').register({ - project: 'e2e' + project: 'e2e/tsconfig.e2e.json' }); }, onPrepare() { From a4609e5bab8816ca3e4264b1aeefb49bad580e95 Mon Sep 17 00:00:00 2001 From: Sharikov Vladislav Date: Tue, 8 Aug 2017 17:03:19 +0300 Subject: [PATCH 14/67] chore(docs): Fix spyOn arguments example (#249) --- docs/store/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/store/testing.md b/docs/store/testing.md index 746251b924..4446494727 100644 --- a/docs/store/testing.md +++ b/docs/store/testing.md @@ -71,7 +71,7 @@ describe('My Component', () => { store = TestBed.get(Store); - spyOn(store.dispatch).and.callThrough(); + spyOn(store, 'dispatch').and.callThrough(); fixture = TestBed.createComponent(MyComponent); component = fixture.componentInstance; From 0fc1bcc2674eb0a755fcd64b576f244f25d7df41 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 8 Aug 2017 20:43:59 -0500 Subject: [PATCH 15/67] feat(RouterStore): Add serializer for router state snapshot (#188) This adds a serializer that can be customized for returning the router state snapshot. By default, the entire RouterStateSnapshot is returned. Documentation has been updated with example usage. ```ts import { StoreModule } from '@ngrx/store'; import { StoreRouterConnectingModule, routerReducer, RouterStateSerializer } from '@ngrx/router-store'; export interface RouterStateUrl { url: string; } export class CustomSerializer implements RouterStateSerializer { serialize(routerState: RouterStateSnapshot): RouterStateUrl { const url = routerState ? routerState.url : ''; // Only return an object including the URL // instead of the entire snapshot return { url }; } } @NgModule({ imports: [ StoreModule.forRoot({ routerReducer: routerReducer }), RouterModule.forRoot([ // routes ]), StoreRouterConnectingModule ], providers: [ { provide: RouterStateSerializer, useClass: CustomSerializer } ] }) export class AppModule { } ``` Closes #97, #104, #237 --- docs/router-store/README.md | 7 ++- docs/router-store/api.md | 46 +++++++++++++++ example-app/app/app.module.ts | 14 ++++- example-app/app/reducers/index.ts | 3 + example-app/app/shared/utils.ts | 24 ++++++++ modules/router-store/spec/integration.spec.ts | 57 ++++++++++++++++++- modules/router-store/src/index.ts | 5 ++ .../router-store/src/router_store_module.ts | 40 +++++++++---- modules/router-store/src/serializer.ts | 13 +++++ 9 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 docs/router-store/api.md create mode 100644 example-app/app/shared/utils.ts create mode 100644 modules/router-store/src/serializer.ts diff --git a/docs/router-store/README.md b/docs/router-store/README.md index 0380d93c67..18c731954c 100644 --- a/docs/router-store/README.md +++ b/docs/router-store/README.md @@ -15,8 +15,8 @@ Install @ngrx/router-store from npm: During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action, which has the following signature: ```ts -export type RouterNavigationPayload = { - routerState: RouterStateSnapshot, +export type RouterNavigationPayload = { + routerState: T, event: RoutesRecognized } ``` @@ -46,3 +46,6 @@ import { App } from './app.component'; }) export class AppModule { } ``` + +## API Documentation +- [Custom Router State Serializer](./api.md#custom-router-state-serializer) \ No newline at end of file diff --git a/docs/router-store/api.md b/docs/router-store/api.md new file mode 100644 index 0000000000..b9eca6d52e --- /dev/null +++ b/docs/router-store/api.md @@ -0,0 +1,46 @@ +# API + +## Custom Router State Serializer + +During each navigation cycle, a `RouterNavigationAction` is dispatched with a snapshot of the state in its payload, the `RouterStateSnapshot`. The `RouterStateSnapshot` is a large complex structure, containing many pieces of information about the current state and what's rendered by the router. This can cause performance +issues when used with the Store Devtools. In most cases, you may only need a piece of information from the `RouterStateSnapshot`. In order to pair down the `RouterStateSnapshot` provided during navigation, you provide a custom serializer for the snapshot to only return what you need to be added to the payload and store. + +To use the time-traveling debugging in the Devtools, you must return an object containing the `url` when using the `routerReducer`. + +```ts +import { StoreModule } from '@ngrx/store'; +import { + StoreRouterConnectingModule, + routerReducer, + RouterStateSerializer, + RouterStateSnapshotType +} from '@ngrx/router-store'; + +export interface RouterStateUrl { + url: string; +} + +export class CustomSerializer implements RouterStateSerializer { + serialize(routerState: RouterStateSnapshot): RouterStateUrl { + const { url } = routerState; + + // Only return an object including the URL + // instead of the entire snapshot + return { url }; + } +} + +@NgModule({ + imports: [ + StoreModule.forRoot({ routerReducer: routerReducer }), + RouterModule.forRoot([ + // routes + ]), + StoreRouterConnectingModule + ], + providers: [ + { provide: RouterStateSerializer, useClass: CustomSerializer } + ] +}) +export class AppModule { } +``` \ No newline at end of file diff --git a/example-app/app/app.module.ts b/example-app/app/app.module.ts index 6eac29b6d2..4eac09b73b 100644 --- a/example-app/app/app.module.ts +++ b/example-app/app/app.module.ts @@ -8,7 +8,10 @@ import { HttpModule } from '@angular/http'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { DBModule } from '@ngrx/db'; -import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { + StoreRouterConnectingModule, + RouterStateSerializer, +} from '@ngrx/router-store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { CoreModule } from './core/core.module'; @@ -17,6 +20,7 @@ import { AuthModule } from './auth/auth.module'; import { routes } from './routes'; import { reducers, metaReducers } from './reducers'; import { schema } from './db'; +import { CustomRouterStateSerializer } from './shared/utils'; import { AppComponent } from './core/containers/app'; import { environment } from '../environments/environment'; @@ -74,6 +78,14 @@ import { environment } from '../environments/environment'; AuthModule.forRoot(), ], + providers: [ + /** + * The `RouterStateSnapshot` provided by the `Router` is a large complex structure. + * A custom RouterStateSerializer is used to parse the `RouterStateSnapshot` provided + * by `@ngrx/router-store` to include only the desired pieces of the snapshot. + */ + { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }, + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/example-app/app/reducers/index.ts b/example-app/app/reducers/index.ts index 1f04960303..0833cd6774 100644 --- a/example-app/app/reducers/index.ts +++ b/example-app/app/reducers/index.ts @@ -5,6 +5,7 @@ import { ActionReducer, } from '@ngrx/store'; import { environment } from '../../environments/environment'; +import * as fromRouter from '@ngrx/router-store'; /** * Every reducer module's default export is the reducer function itself. In @@ -21,6 +22,7 @@ import * as fromLayout from '../core/reducers/layout'; */ export interface State { layout: fromLayout.State; + routerReducer: fromRouter.RouterReducerState; } /** @@ -30,6 +32,7 @@ export interface State { */ export const reducers: ActionReducerMap = { layout: fromLayout.reducer, + routerReducer: fromRouter.routerReducer, }; // console.log all actions diff --git a/example-app/app/shared/utils.ts b/example-app/app/shared/utils.ts new file mode 100644 index 0000000000..758011bce2 --- /dev/null +++ b/example-app/app/shared/utils.ts @@ -0,0 +1,24 @@ +import { RouterStateSerializer } from '@ngrx/router-store'; +import { RouterStateSnapshot } from '@angular/router'; + +/** + * The RouterStateSerializer takes the current RouterStateSnapshot + * and returns any pertinent information needed. The snapshot contains + * all information about the state of the router at the given point in time. + * The entire snapshot is complex and not always needed. In this case, you only + * need the URL from the snapshot in the store. Other items could be + * returned such as route parameters, query parameters and static route data. + */ + +export interface RouterStateUrl { + url: string; +} + +export class CustomRouterStateSerializer + implements RouterStateSerializer { + serialize(routerState: RouterStateSnapshot): RouterStateUrl { + const { url } = routerState; + + return { url }; + } +} diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index 4a16632a26..c0c53184a0 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -1,6 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, Provider } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { NavigationEnd, Router } from '@angular/router'; +import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Store, StoreModule } from '@ngrx/store'; import { @@ -10,6 +10,7 @@ import { RouterAction, routerReducer, StoreRouterConnectingModule, + RouterStateSerializer, } from '../src/index'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/first'; @@ -315,11 +316,60 @@ describe('integration spec', () => { ]); done(); }); + + it('should support a custom RouterStateSnapshot serializer ', done => { + const reducer = (state: any, action: RouterAction) => { + const r = routerReducer(state, action); + return r && r.state + ? { url: r.state.url, navigationId: r.navigationId } + : null; + }; + + class CustomSerializer implements RouterStateSerializer<{ url: string }> { + serialize(routerState: RouterStateSnapshot) { + const url = `${routerState.url}-custom`; + + return { url }; + } + } + + const providers = [ + { provide: RouterStateSerializer, useClass: CustomSerializer }, + ]; + + createTestModule({ reducers: { routerReducer, reducer }, providers }); + + const router = TestBed.get(Router); + const store = TestBed.get(Store); + const log = logOfRouterAndStore(router, store); + + router + .navigateByUrl('/') + .then(() => { + log.splice(0); + return router.navigateByUrl('next'); + }) + .then(() => { + expect(log).toEqual([ + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'store', state: { url: '/next-custom', navigationId: 2 } }, + { type: 'router', event: 'NavigationEnd', url: '/next' }, + ]); + log.splice(0); + done(); + }); + }); }); }); function createTestModule( - opts: { reducers?: any; canActivate?: Function; canLoad?: Function } = {} + opts: { + reducers?: any; + canActivate?: Function; + canLoad?: Function; + providers?: Provider[]; + } = {} ) { @Component({ selector: 'test-app', @@ -361,6 +411,7 @@ function createTestModule( provide: 'CanLoadNext', useValue: opts.canLoad || (() => true), }, + opts.providers || [], ], }); diff --git a/modules/router-store/src/index.ts b/modules/router-store/src/index.ts index bcb9e2e004..ef9b177f2e 100644 --- a/modules/router-store/src/index.ts +++ b/modules/router-store/src/index.ts @@ -13,3 +13,8 @@ export { RouterNavigationPayload, StoreRouterConnectingModule, } from './router_store_module'; + +export { + RouterStateSerializer, + DefaultRouterStateSerializer, +} from './serializer'; diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 1ee0481841..000ab7608a 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -8,7 +8,10 @@ import { } from '@angular/router'; import { Store } from '@ngrx/store'; import { of } from 'rxjs/observable/of'; - +import { + DefaultRouterStateSerializer, + RouterStateSerializer, +} from './serializer'; /** * An action dispatched when the router navigates. */ @@ -17,17 +20,17 @@ export const ROUTER_NAVIGATION = 'ROUTER_NAVIGATION'; /** * Payload of ROUTER_NAVIGATION. */ -export type RouterNavigationPayload = { - routerState: RouterStateSnapshot; +export type RouterNavigationPayload = { + routerState: T; event: RoutesRecognized; }; /** * An action dispatched when the router navigates. */ -export type RouterNavigationAction = { +export type RouterNavigationAction = { type: typeof ROUTER_NAVIGATION; - payload: RouterNavigationPayload; + payload: RouterNavigationPayload; }; /** @@ -78,7 +81,7 @@ export type RouterErrorAction = { * An union type of router actions. */ export type RouterAction = - | RouterNavigationAction + | RouterNavigationAction | RouterCancelAction | RouterErrorAction; @@ -133,7 +136,7 @@ export function routerReducer( * declarations: [AppCmp, SimpleCmp], * imports: [ * BrowserModule, - * StoreModule.provideStore(mapOfReducers), + * StoreModule.forRoot(mapOfReducers), * RouterModule.forRoot([ * { path: '', component: SimpleCmp }, * { path: 'next', component: SimpleCmp } @@ -146,16 +149,24 @@ export function routerReducer( * } * ``` */ -@NgModule({}) +@NgModule({ + providers: [ + { provide: RouterStateSerializer, useClass: DefaultRouterStateSerializer }, + ], +}) export class StoreRouterConnectingModule { - private routerState: RouterStateSnapshot | null = null; + private routerState: RouterStateSnapshot; private storeState: any; private lastRoutesRecognized: RoutesRecognized; private dispatchTriggeredByRouter: boolean = false; // used only in dev mode in combination with routerReducer private navigationTriggeredByDispatch: boolean = false; // used only in dev mode in combination with routerReducer - constructor(private store: Store, private router: Router) { + constructor( + private store: Store, + private router: Router, + private serializer: RouterStateSerializer + ) { this.setUpBeforePreactivationHook(); this.setUpStoreStateListener(); this.setUpStateRollbackEvents(); @@ -165,7 +176,7 @@ export class StoreRouterConnectingModule { (this.router).hooks.beforePreactivation = ( routerState: RouterStateSnapshot ) => { - this.routerState = routerState; + this.routerState = this.serializer.serialize(routerState); if (this.shouldDispatchRouterNavigation()) this.dispatchRouterNavigation(); return of(true); @@ -214,7 +225,12 @@ export class StoreRouterConnectingModule { private dispatchRouterNavigation(): void { this.dispatchRouterAction(ROUTER_NAVIGATION, { routerState: this.routerState, - event: this.lastRoutesRecognized, + event: { + id: this.lastRoutesRecognized.id, + url: this.lastRoutesRecognized.url, + urlAfterRedirects: this.lastRoutesRecognized.urlAfterRedirects, + state: this.serializer.serialize(this.routerState), + } as RoutesRecognized, }); } diff --git a/modules/router-store/src/serializer.ts b/modules/router-store/src/serializer.ts new file mode 100644 index 0000000000..ddeecad0b9 --- /dev/null +++ b/modules/router-store/src/serializer.ts @@ -0,0 +1,13 @@ +import { InjectionToken } from '@angular/core'; +import { RouterStateSnapshot } from '@angular/router'; + +export abstract class RouterStateSerializer { + abstract serialize(routerState: RouterStateSnapshot): T; +} + +export class DefaultRouterStateSerializer + implements RouterStateSerializer { + serialize(routerState: RouterStateSnapshot) { + return routerState; + } +} From c4092521d7cb23e1fb954dd24c08f3e3e309eafd Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 9 Aug 2017 14:41:41 -0500 Subject: [PATCH 16/67] fix(Store): Use existing reducers when providing reducers without an InjectionToken (#254) Closes #250, related to #116 --- modules/store/src/store_module.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index bcccb071bd..a8f98a34f7 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -107,9 +107,11 @@ export class StoreModule { deps: [_INITIAL_STATE], }, { provide: _INITIAL_REDUCERS, useValue: reducers }, - reducers instanceof InjectionToken - ? [{ provide: _STORE_REDUCERS, useExisting: reducers }] - : [], + { + provide: _STORE_REDUCERS, + useExisting: + reducers instanceof InjectionToken ? reducers : _INITIAL_REDUCERS, + }, { provide: INITIAL_REDUCERS, deps: [ From bd968fa2ae0ee421a989a325e7f4ecc8a7ba0dd9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 10 Aug 2017 15:25:26 -0500 Subject: [PATCH 17/67] fix(Store): Use injector to get reducers provided via InjectionTokens (#259) The injector behavior is different when compiled through AOT such that provided tokens aren't resolved before the factory function is executed. This fix uses the injector to get the reducers provided through tokens. Reference #189 --- modules/store/src/store_module.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index a8f98a34f7..f21bb7ad51 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -4,7 +4,7 @@ import { ModuleWithProviders, OnDestroy, InjectionToken, - Optional, + Injector, } from '@angular/core'; import { Action, @@ -114,10 +114,7 @@ export class StoreModule { }, { provide: INITIAL_REDUCERS, - deps: [ - _INITIAL_REDUCERS, - [new Optional(), new Inject(_STORE_REDUCERS)], - ], + deps: [Injector, _INITIAL_REDUCERS, [new Inject(_STORE_REDUCERS)]], useFactory: _createStoreReducers, }, { @@ -185,8 +182,9 @@ export class StoreModule { provide: FEATURE_REDUCERS, multi: true, deps: [ + Injector, _FEATURE_REDUCERS, - [new Optional(), new Inject(_FEATURE_REDUCERS_TOKEN)], + [new Inject(_FEATURE_REDUCERS_TOKEN)], ], useFactory: _createFeatureReducers, }, @@ -196,20 +194,20 @@ export class StoreModule { } export function _createStoreReducers( + injector: Injector, reducers: ActionReducerMap, tokenReducers: ActionReducerMap ) { - return reducers instanceof InjectionToken ? tokenReducers : reducers; + return reducers instanceof InjectionToken ? injector.get(reducers) : reducers; } export function _createFeatureReducers( + injector: Injector, reducerCollection: ActionReducerMap[], tokenReducerCollection: ActionReducerMap[] ) { return reducerCollection.map((reducer, index) => { - return reducer instanceof InjectionToken - ? tokenReducerCollection[index] - : reducer; + return reducer instanceof InjectionToken ? injector.get(reducer) : reducer; }); } From 683013c47acc6446e7ea88872ded7f6a9d1fb867 Mon Sep 17 00:00:00 2001 From: Reko Jokelainen Date: Fri, 11 Aug 2017 16:51:23 +0300 Subject: [PATCH 18/67] fix(Store): Update usage of compose for reducer factory (#252) Closes #247 --- modules/store/spec/modules.spec.ts | 33 +++++++++++++++ modules/store/spec/utils.spec.ts | 65 +++++++++++++++++++++++++++++- modules/store/src/utils.ts | 5 ++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/modules/store/spec/modules.spec.ts b/modules/store/spec/modules.spec.ts index 5a6690c691..019896fbb0 100644 --- a/modules/store/spec/modules.spec.ts +++ b/modules/store/spec/modules.spec.ts @@ -119,6 +119,39 @@ describe(`Store Modules`, () => { }); }); + describe(`: With initial state`, () => { + const initialState: RootState = { fruit: 'banana' }; + const reducerMap: ActionReducerMap = { fruit: rootFruitReducer }; + const noopMetaReducer = (r: Function) => (state: any, action: any) => { + return r(state, action); + }; + + const testWithMetaReducers = (metaReducers: any[]) => () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot(reducerMap, { initialState, metaReducers }), + ], + }); + store = TestBed.get(Store); + }); + it('should have initial state', () => { + store.take(1).subscribe((s: any) => { + expect(s).toEqual(initialState); + }); + }); + }; + + describe( + 'should add initial state with no meta reducers', + testWithMetaReducers([]) + ); + describe( + 'should add initial state with a simple no-op meta reducer', + testWithMetaReducers([noopMetaReducer]) + ); + }); + describe(`: Nested`, () => { @NgModule({ imports: [StoreModule.forFeature('a', featureAReducer)], diff --git a/modules/store/spec/utils.spec.ts b/modules/store/spec/utils.spec.ts index 9b2cd7ea28..63d6407198 100644 --- a/modules/store/spec/utils.spec.ts +++ b/modules/store/spec/utils.spec.ts @@ -1,5 +1,11 @@ import { omit } from '../src/utils'; -import { combineReducers, compose } from '@ngrx/store'; +import { + ActionReducer, + ActionReducerMap, + combineReducers, + compose, + createReducerFactory, +} from '@ngrx/store'; describe(`Store utils`, () => { describe(`combineReducers()`, () => { @@ -73,4 +79,61 @@ describe(`Store utils`, () => { expect(id(1)).toBe(1); }); }); + + describe(`createReducerFactory()`, () => { + const fruitReducer = (state: string = 'banana', action: any) => + action.type === 'fruit' ? action.payload : state; + type FruitState = { fruit: string }; + const reducerMap: ActionReducerMap = { fruit: fruitReducer }; + const initialState: FruitState = { fruit: 'apple' }; + + const runWithExpectations = ( + metaReducers: any[], + initialState: any, + expectedState: any + ) => () => { + let spiedFactory: jasmine.Spy; + let reducer: ActionReducer; + beforeEach(() => { + spiedFactory = jasmine + .createSpy('spied factory') + .and.callFake(combineReducers); + reducer = createReducerFactory(spiedFactory, metaReducers)( + reducerMap, + initialState + ); + }); + it(`should pass the reducers and initialState to the factory method`, () => { + expect(spiedFactory).toHaveBeenCalledWith(reducerMap, initialState); + }); + it(`should return the expected initialState`, () => { + expect(reducer(undefined, { type: 'init' })).toEqual(expectedState); + }); + }; + + describe(`without meta reducers`, () => { + const metaReducers: any[] = []; + describe( + `with initial state`, + runWithExpectations(metaReducers, initialState, initialState) + ); + describe( + `without initial state`, + runWithExpectations(metaReducers, undefined, { fruit: 'banana' }) + ); + }); + + describe(`with meta reducers`, () => { + const noopMetaReducer = (r: any) => r; + const metaReducers: any[] = [noopMetaReducer]; + describe( + `with initial state`, + runWithExpectations(metaReducers, initialState, initialState) + ); + describe( + `without initial state`, + runWithExpectations(metaReducers, undefined, { fruit: 'banana' }) + ); + }); + }); }); diff --git a/modules/store/src/utils.ts b/modules/store/src/utils.ts index a8a30c7642..d1812a355c 100644 --- a/modules/store/src/utils.ts +++ b/modules/store/src/utils.ts @@ -89,7 +89,10 @@ export function createReducerFactory( metaReducers?: ActionReducer[] ): ActionReducerFactory { if (Array.isArray(metaReducers) && metaReducers.length > 0) { - return compose.apply(null, [...metaReducers, reducerFactory]); + return compose(...metaReducers)(reducerFactory) as ActionReducerFactory< + any, + any + >; } return reducerFactory; From ca544dd07acfdf399f2183383e973749b94ba121 Mon Sep 17 00:00:00 2001 From: Juan Herrera Date: Fri, 11 Aug 2017 08:52:40 -0500 Subject: [PATCH 19/67] chore(docs): Fix selection of state using string selector (#261) --- docs/store/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/store/README.md b/docs/store/README.md index b3bd1ad5f0..3f90a4d38e 100644 --- a/docs/store/README.md +++ b/docs/store/README.md @@ -96,7 +96,7 @@ export class MyAppComponent { counter: Observable; constructor(private store: Store) { - this.counter = store.select('counter'); + this.counter = store.select('counter'); } increment(){ From 1cbb2c9965201324b9e5ca34fa76d4de32d2ff45 Mon Sep 17 00:00:00 2001 From: Daniel Karp Date: Sun, 13 Aug 2017 01:00:49 -0400 Subject: [PATCH 20/67] fix(Effects): Deprecate toPayload utility function (#266) --- docs/effects/api.md | 17 +++++++++++++---- example-app/app/books/effects/book.ts | 12 +++--------- modules/effects/src/util.ts | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/effects/api.md b/docs/effects/api.md index ffdbf151a0..1f618cb581 100644 --- a/docs/effects/api.md +++ b/docs/effects/api.md @@ -55,7 +55,7 @@ export class SomeEffectsClass { ### ofType -Filter actions by action types. +Filter actions by action types. Specify the action type to allow type-safe mapping to other data on the action, including payload. Usage: ```ts @@ -67,7 +67,7 @@ import { Actions, Effect } from '@ngrx/effects'; export class SomeEffectsClass { constructor(private actions$: Actions) {} - @Effect() authActions$ = this.action$.ofType('LOGIN', 'LOGOUT') + @Effect() authActions$ = this.action$.ofType('LOGIN', 'LOGOUT') .do(action => { console.log(action); }); @@ -128,8 +128,8 @@ export class UserEffects implements OnRunEffects { ## Utilities -### toPayload -Maps an action to its payload. +### toPayload (DEPRECATED) +Maps an action to its payload. This function is deprecated, and will be removed in version 5.0. Usage: ```ts @@ -150,6 +150,15 @@ export class SomeEffectsClass { } ``` +Recommended alternative to deprecated toPayload function. Note that the type +of the action is specified so that mapping to payload (or whatever data is available in the action) is type-safe. +```ts + @Effect() authActions$ = this.action$.ofType('LOGIN', 'LOGOUT') + .map(action => action.payload) + .do(payload => { + console.log(payload); +``` + ### mergeEffects Manually merges all decorated effects into a combined observable. diff --git a/example-app/app/books/effects/book.ts b/example-app/app/books/effects/book.ts index f660018d97..76cee2e628 100644 --- a/example-app/app/books/effects/book.ts +++ b/example-app/app/books/effects/book.ts @@ -5,7 +5,7 @@ import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/skip'; import 'rxjs/add/operator/takeUntil'; import { Injectable, InjectionToken, Optional, Inject } from '@angular/core'; -import { Effect, Actions, toPayload } from '@ngrx/effects'; +import { Effect, Actions } from '@ngrx/effects'; import { Action } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { Scheduler } from 'rxjs/Scheduler'; @@ -25,12 +25,6 @@ export const SEARCH_SCHEDULER = new InjectionToken( /** * Effects offer a way to isolate and easily test side-effects within your * application. - * The `toPayload` helper function returns just - * the payload of the currently dispatched action, useful in - * instances where the current state is not necessary. - * - * Documentation on `toPayload` can be found here: - * https://github.com/ngrx/platform/blob/master/docs/effects/api.md#topayload * * If you are unfamiliar with the operators being used in these examples, please * check out the sources below: @@ -43,9 +37,9 @@ export const SEARCH_SCHEDULER = new InjectionToken( export class BookEffects { @Effect() search$: Observable = this.actions$ - .ofType(book.SEARCH) + .ofType(book.SEARCH) .debounceTime(this.debounce, this.scheduler || async) - .map(toPayload) + .map(action => action.payload) .switchMap(query => { if (query === '') { return empty(); diff --git a/modules/effects/src/util.ts b/modules/effects/src/util.ts index 6c695ac396..7d16afe501 100644 --- a/modules/effects/src/util.ts +++ b/modules/effects/src/util.ts @@ -1,5 +1,8 @@ import { Action } from '@ngrx/store'; +/** + * @deprecated Since version 4.1. Will be deleted in version 5.0. + */ export function toPayload(action: Action): any { return (action as any).payload; } From 657a2e33774fd070c1be18c68e3a0396816608e2 Mon Sep 17 00:00:00 2001 From: BrainCrumbz Date: Sun, 13 Aug 2017 07:01:15 +0200 Subject: [PATCH 21/67] docs(templates): fix typo in issue template (#262) --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 322a2870b2..be0f04fe80 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -11,7 +11,7 @@ ## What is the current behavior? - + ## Expected behavior: From 9e33a5de34ed4d360faba39423ffa848cfe1974d Mon Sep 17 00:00:00 2001 From: Pierre Rochard Date: Sun, 13 Aug 2017 11:22:28 -0400 Subject: [PATCH 22/67] docs(Migration): Fix typo in Router Store migration example --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 90e89378d6..84e72965ee 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -458,7 +458,7 @@ export class Back implements Action { } export class Forward implements Action { - readonly type = FOWARD; + readonly type = FORWARD; } export type Actions From 57633d2a8fc3de6f3f7e6ec38d8c9f83d0c86f73 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 14 Aug 2017 21:34:49 -0500 Subject: [PATCH 23/67] fix(Store): Add type signature for metareducer (#270) Closes #264, Related to #170 --- docs/store/api.md | 6 +++--- example-app/app/reducers/index.ts | 5 +++-- modules/store/spec/utils.spec.ts | 3 ++- modules/store/src/index.ts | 1 + modules/store/src/models.ts | 6 +++++- modules/store/src/store_module.ts | 3 ++- modules/store/src/utils.ts | 9 +++++---- 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/store/api.md b/docs/store/api.md index 4fee0303ba..7d7758c0ec 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -49,11 +49,11 @@ export function getInitialState() { configuration option to provide an array of meta-reducers that are composed from right to left. ```ts -import { StoreModule, combineReducers, compose } from '@ngrx/store'; +import { StoreModule, ActionReducer } from '@ngrx/store'; import { reducers } from './reducers'; // console.log all actions -function debug(reducer) { +export function debug(reducer: ActionReducer): ActionReducer { return function(state, action) { console.log('state', state); console.log('action', action); @@ -62,7 +62,7 @@ function debug(reducer) { } } -const metaReducers = [debug]; +export const metaReducers = [debug]; @NgModule({ imports: [ diff --git a/example-app/app/reducers/index.ts b/example-app/app/reducers/index.ts index 0833cd6774..cb26504f4a 100644 --- a/example-app/app/reducers/index.ts +++ b/example-app/app/reducers/index.ts @@ -3,6 +3,7 @@ import { createSelector, createFeatureSelector, ActionReducer, + MetaReducer, } from '@ngrx/store'; import { environment } from '../../environments/environment'; import * as fromRouter from '@ngrx/router-store'; @@ -36,7 +37,7 @@ export const reducers: ActionReducerMap = { }; // console.log all actions -export function logger(reducer: ActionReducer): ActionReducer { +export function logger(reducer: ActionReducer): ActionReducer { return function(state: State, action: any): State { console.log('state', state); console.log('action', action); @@ -50,7 +51,7 @@ export function logger(reducer: ActionReducer): ActionReducer { * the root meta-reducer. To add more meta-reducers, provide an array of meta-reducers * that will be composed to form the root meta-reducer. */ -export const metaReducers: ActionReducer[] = !environment.production +export const metaReducers: MetaReducer[] = !environment.production ? [logger] : []; diff --git a/modules/store/spec/utils.spec.ts b/modules/store/spec/utils.spec.ts index 63d6407198..a141dc5938 100644 --- a/modules/store/spec/utils.spec.ts +++ b/modules/store/spec/utils.spec.ts @@ -5,6 +5,7 @@ import { combineReducers, compose, createReducerFactory, + MetaReducer } from '@ngrx/store'; describe(`Store utils`, () => { @@ -88,7 +89,7 @@ describe(`Store utils`, () => { const initialState: FruitState = { fruit: 'apple' }; const runWithExpectations = ( - metaReducers: any[], + metaReducers: MetaReducer[], initialState: any, expectedState: any ) => () => { diff --git a/modules/store/src/index.ts b/modules/store/src/index.ts index 054c8576f6..607a423414 100644 --- a/modules/store/src/index.ts +++ b/modules/store/src/index.ts @@ -3,6 +3,7 @@ export { ActionReducer, ActionReducerMap, ActionReducerFactory, + MetaReducer, Selector, } from './models'; export { StoreModule } from './store_module'; diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts index 4c5c4c324f..ef6acfbd78 100644 --- a/modules/store/src/models.ts +++ b/modules/store/src/models.ts @@ -21,12 +21,16 @@ export interface ActionReducerFactory { ): ActionReducer; } +export type MetaReducer = ( + reducer: ActionReducer +) => ActionReducer; + export interface StoreFeature { key: string; reducers: ActionReducerMap | ActionReducer; reducerFactory: ActionReducerFactory; initialState?: InitialState; - metaReducers?: ActionReducer[]; + metaReducers?: MetaReducer[]; } export interface Selector { diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index f21bb7ad51..0481dd2ad8 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -13,6 +13,7 @@ import { ActionReducerFactory, StoreFeature, InitialState, + MetaReducer, } from './models'; import { compose, combineReducers, createReducerFactory } from './utils'; import { @@ -82,7 +83,7 @@ export class StoreFeatureModule implements OnDestroy { export type StoreConfig = { initialState?: InitialState; reducerFactory?: ActionReducerFactory; - metaReducers?: ActionReducer[]; + metaReducers?: MetaReducer[]; }; @NgModule({}) diff --git a/modules/store/src/utils.ts b/modules/store/src/utils.ts index d1812a355c..77d86db5a8 100644 --- a/modules/store/src/utils.ts +++ b/modules/store/src/utils.ts @@ -3,6 +3,7 @@ import { ActionReducer, ActionReducerMap, ActionReducerFactory, + MetaReducer, } from './models'; export function combineReducers( @@ -84,10 +85,10 @@ export function compose(...functions: any[]) { }; } -export function createReducerFactory( - reducerFactory: ActionReducerFactory, - metaReducers?: ActionReducer[] -): ActionReducerFactory { +export function createReducerFactory( + reducerFactory: ActionReducerFactory, + metaReducers?: MetaReducer[] +): ActionReducerFactory { if (Array.isArray(metaReducers) && metaReducers.length > 0) { return compose(...metaReducers)(reducerFactory) as ActionReducerFactory< any, From 71690fb9ad4bcb4e437e5401ebc2e0785c156060 Mon Sep 17 00:00:00 2001 From: Wayne Maurer Date: Tue, 15 Aug 2017 14:14:27 +0200 Subject: [PATCH 24/67] chore(docs): Fix typo in router store readme (#271) --- docs/router-store/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/router-store/README.md b/docs/router-store/README.md index 18c731954c..6bb08e2239 100644 --- a/docs/router-store/README.md +++ b/docs/router-store/README.md @@ -21,7 +21,7 @@ export type RouterNavigationPayload = { } ``` -- Reducers recieve this action. Throwing an error in the reducer cancels navigation. +- Reducers receive this action. Throwing an error in the reducer cancels navigation. - Effects can listen for this action. - The `ROUTER_CANCEL` action represents a guard canceling navigation. - A `ROUTER_ERROR` action represents a navigation error . @@ -48,4 +48,4 @@ export class AppModule { } ``` ## API Documentation -- [Custom Router State Serializer](./api.md#custom-router-state-serializer) \ No newline at end of file +- [Custom Router State Serializer](./api.md#custom-router-state-serializer) From dff9509d8d4e89c157c983bea5c2a374f91fe762 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 16 Aug 2017 13:46:01 -0500 Subject: [PATCH 25/67] Revert "fix(Store): Update usage of compose for reducer factory (#252)" (#277) This reverts commit 683013c47acc6446e7ea88872ded7f6a9d1fb867. --- modules/store/spec/modules.spec.ts | 33 --------------- modules/store/spec/utils.spec.ts | 66 +----------------------------- modules/store/src/utils.ts | 5 +-- 3 files changed, 2 insertions(+), 102 deletions(-) diff --git a/modules/store/spec/modules.spec.ts b/modules/store/spec/modules.spec.ts index 019896fbb0..5a6690c691 100644 --- a/modules/store/spec/modules.spec.ts +++ b/modules/store/spec/modules.spec.ts @@ -119,39 +119,6 @@ describe(`Store Modules`, () => { }); }); - describe(`: With initial state`, () => { - const initialState: RootState = { fruit: 'banana' }; - const reducerMap: ActionReducerMap = { fruit: rootFruitReducer }; - const noopMetaReducer = (r: Function) => (state: any, action: any) => { - return r(state, action); - }; - - const testWithMetaReducers = (metaReducers: any[]) => () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot(reducerMap, { initialState, metaReducers }), - ], - }); - store = TestBed.get(Store); - }); - it('should have initial state', () => { - store.take(1).subscribe((s: any) => { - expect(s).toEqual(initialState); - }); - }); - }; - - describe( - 'should add initial state with no meta reducers', - testWithMetaReducers([]) - ); - describe( - 'should add initial state with a simple no-op meta reducer', - testWithMetaReducers([noopMetaReducer]) - ); - }); - describe(`: Nested`, () => { @NgModule({ imports: [StoreModule.forFeature('a', featureAReducer)], diff --git a/modules/store/spec/utils.spec.ts b/modules/store/spec/utils.spec.ts index a141dc5938..9b2cd7ea28 100644 --- a/modules/store/spec/utils.spec.ts +++ b/modules/store/spec/utils.spec.ts @@ -1,12 +1,5 @@ import { omit } from '../src/utils'; -import { - ActionReducer, - ActionReducerMap, - combineReducers, - compose, - createReducerFactory, - MetaReducer -} from '@ngrx/store'; +import { combineReducers, compose } from '@ngrx/store'; describe(`Store utils`, () => { describe(`combineReducers()`, () => { @@ -80,61 +73,4 @@ describe(`Store utils`, () => { expect(id(1)).toBe(1); }); }); - - describe(`createReducerFactory()`, () => { - const fruitReducer = (state: string = 'banana', action: any) => - action.type === 'fruit' ? action.payload : state; - type FruitState = { fruit: string }; - const reducerMap: ActionReducerMap = { fruit: fruitReducer }; - const initialState: FruitState = { fruit: 'apple' }; - - const runWithExpectations = ( - metaReducers: MetaReducer[], - initialState: any, - expectedState: any - ) => () => { - let spiedFactory: jasmine.Spy; - let reducer: ActionReducer; - beforeEach(() => { - spiedFactory = jasmine - .createSpy('spied factory') - .and.callFake(combineReducers); - reducer = createReducerFactory(spiedFactory, metaReducers)( - reducerMap, - initialState - ); - }); - it(`should pass the reducers and initialState to the factory method`, () => { - expect(spiedFactory).toHaveBeenCalledWith(reducerMap, initialState); - }); - it(`should return the expected initialState`, () => { - expect(reducer(undefined, { type: 'init' })).toEqual(expectedState); - }); - }; - - describe(`without meta reducers`, () => { - const metaReducers: any[] = []; - describe( - `with initial state`, - runWithExpectations(metaReducers, initialState, initialState) - ); - describe( - `without initial state`, - runWithExpectations(metaReducers, undefined, { fruit: 'banana' }) - ); - }); - - describe(`with meta reducers`, () => { - const noopMetaReducer = (r: any) => r; - const metaReducers: any[] = [noopMetaReducer]; - describe( - `with initial state`, - runWithExpectations(metaReducers, initialState, initialState) - ); - describe( - `without initial state`, - runWithExpectations(metaReducers, undefined, { fruit: 'banana' }) - ); - }); - }); }); diff --git a/modules/store/src/utils.ts b/modules/store/src/utils.ts index 77d86db5a8..fb1d48a24c 100644 --- a/modules/store/src/utils.ts +++ b/modules/store/src/utils.ts @@ -90,10 +90,7 @@ export function createReducerFactory( metaReducers?: MetaReducer[] ): ActionReducerFactory { if (Array.isArray(metaReducers) && metaReducers.length > 0) { - return compose(...metaReducers)(reducerFactory) as ActionReducerFactory< - any, - any - >; + return compose.apply(null, [...metaReducers, reducerFactory]); } return reducerFactory; From 02d6ad10eaf46960fb8a2ae0895bd6af60101572 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Wed, 16 Aug 2017 13:48:31 -0500 Subject: [PATCH 26/67] v4.0.3 --- lerna.json | 2 +- modules/effects/CHANGELOG.md | 99 ++++++++++++++++++++++++++++++- modules/effects/package.json | 2 +- modules/router-store/CHANGELOG.md | 27 +++++++++ modules/router-store/package.json | 2 +- modules/store/CHANGELOG.md | 94 ++++++++++++++++++++++++++++- modules/store/package.json | 2 +- 7 files changed, 220 insertions(+), 8 deletions(-) diff --git a/lerna.json b/lerna.json index 85a01f3e10..ce3400f414 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "modules/*" ], - "version": "4.0.2", + "version": "4.0.3", "npmClient": "yarn" } diff --git a/modules/effects/CHANGELOG.md b/modules/effects/CHANGELOG.md index eb3580ae7e..4b6da68156 100644 --- a/modules/effects/CHANGELOG.md +++ b/modules/effects/CHANGELOG.md @@ -3,13 +3,42 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -# 4.0.0 (2017-07-18) + +## 4.0.3 (2017-08-16) + + +### Bug Fixes + +* **Effects:** Deprecate toPayload utility function (#266) ([1cbb2c9](https://github.com/ngrx/platform/commit/1cbb2c9)) +* **Effects:** Ensure StoreModule is loaded before effects (#230) ([065d33e](https://github.com/ngrx/platform/commit/065d33e)), closes [#184](https://github.com/ngrx/platform/issues/184) [#219](https://github.com/ngrx/platform/issues/219) +* **Effects:** Export EffectsNotification interface (#231) ([2b1a076](https://github.com/ngrx/platform/commit/2b1a076)) + + + + +## 4.0.2 (2017-08-02) + + +### Bug Fixes + +* **Effects:** Wrap testing source in an Actions observable (#121) ([bfdb83b](https://github.com/ngrx/platform/commit/bfdb83b)), closes [#117](https://github.com/ngrx/platform/issues/117) + + +### Features + +* **Effects:** Add generic type to the "ofType" operator ([55c13b2](https://github.com/ngrx/platform/commit/55c13b2)) +* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) + + + + +## 4.0.1 (2017-07-18) ### Bug Fixes * **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) +* **effects:** allow downleveled annotations (#98) ([875b326](https://github.com/ngrx/platform/commit/875b326)), closes [#93](https://github.com/ngrx/platform/issues/93) * **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) * **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) @@ -65,3 +94,69 @@ imports: [ }) export class SomeFeatureModule { } ``` + + + + + +# 4.0.0 (2017-07-18) + + +### Bug Fixes + +* **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) +* **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) +* **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) + + +### Code Refactoring + +* **Effects:** Simplified AP, added better error reporting and effects stream control ([015107f](https://github.com/ngrx/platform/commit/015107f)) + + +### Features + +* **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) +* **Effects:** Ensure effects are only subscribed to once ([089abdc](https://github.com/ngrx/platform/commit/089abdc)) +* **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) + + +### BREAKING CHANGES + +* **Effects:** Effects API for registering effects has been updated to allow for multiple classes to be provided. + +BEFORE: +```ts +@NgModule({ +imports: [ +EffectsModule.run(SourceA), +EffectsModule.run(SourceB) +] +}) +export class AppModule { } +``` + +AFTER: +```ts +@NgModule({ +imports: [ +EffectsModule.forRoot([ + SourceA, + SourceB, + SourceC, +]) +] +}) +export class AppModule { } + +@NgModule({ +imports: [ +EffectsModule.forFeature([ + FeatureSourceA, + FeatureSourceB, + FeatureSourceC, +]) +] +}) +export class SomeFeatureModule { } +``` diff --git a/modules/effects/package.json b/modules/effects/package.json index dd3c28f0ec..831a57b61a 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/effects", - "version": "4.0.2", + "version": "4.0.3", "description": "Side effect model for @ngrx/store", "module": "@ngrx/effects.es5.js", "es2015": "@ngrx/effects.js", diff --git a/modules/router-store/CHANGELOG.md b/modules/router-store/CHANGELOG.md index 25f7c3c950..d32602e733 100644 --- a/modules/router-store/CHANGELOG.md +++ b/modules/router-store/CHANGELOG.md @@ -3,6 +3,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## 4.0.3 (2017-08-16) + + +### Features + +* **RouterStore:** Add serializer for router state snapshot (#188) ([0fc1bcc](https://github.com/ngrx/platform/commit/0fc1bcc)), closes [#97](https://github.com/ngrx/platform/issues/97) [#104](https://github.com/ngrx/platform/issues/104) [#237](https://github.com/ngrx/platform/issues/237) + + + + +## 4.0.2 (2017-08-02) + + +### Bug Fixes + +* **router-store:** NavigationCancel and NavigationError creates a cycle when used with routerReducer ([a085730](https://github.com/ngrx/platform/commit/a085730)), closes [#68](https://github.com/ngrx/platform/issues/68) +* **RouterStore:** Add support for cancellation with CanLoad guard (#223) ([2c006e8](https://github.com/ngrx/platform/commit/2c006e8)), closes [#213](https://github.com/ngrx/platform/issues/213) + + +### Features + +* **router-store:** Added action types (#47) ([1f67cb3](https://github.com/ngrx/platform/commit/1f67cb3)), closes [#44](https://github.com/ngrx/platform/issues/44) + + + + # 4.0.0 (2017-07-18) diff --git a/modules/router-store/package.json b/modules/router-store/package.json index 7869445073..1bd5557dbe 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/router-store", - "version": "4.0.2", + "version": "4.0.3", "description": "Bindings to connect @angular/router to @ngrx/store", "module": "@ngrx/router-store.es5.js", "es2015": "@ngrx/router-store.js", diff --git a/modules/store/CHANGELOG.md b/modules/store/CHANGELOG.md index ed4fc6dbde..988e554040 100644 --- a/modules/store/CHANGELOG.md +++ b/modules/store/CHANGELOG.md @@ -3,16 +3,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -# 4.0.0 (2017-07-18) + +## 4.0.3 (2017-08-16) + + +### Bug Fixes + +* **Store:** Add type signature for metareducer (#270) ([57633d2](https://github.com/ngrx/platform/commit/57633d2)), closes [#264](https://github.com/ngrx/platform/issues/264) [#170](https://github.com/ngrx/platform/issues/170) +* **Store:** Set initial state for feature modules (#235) ([4aec80c](https://github.com/ngrx/platform/commit/4aec80c)), closes [#206](https://github.com/ngrx/platform/issues/206) [#233](https://github.com/ngrx/platform/issues/233) +* **Store:** Update usage of compose for reducer factory (#252) ([683013c](https://github.com/ngrx/platform/commit/683013c)), closes [#247](https://github.com/ngrx/platform/issues/247) +* **Store:** Use existing reducers when providing reducers without an InjectionToken (#254) ([c409252](https://github.com/ngrx/platform/commit/c409252)), closes [#250](https://github.com/ngrx/platform/issues/250) [#116](https://github.com/ngrx/platform/issues/116) +* **Store:** Use injector to get reducers provided via InjectionTokens (#259) ([bd968fa](https://github.com/ngrx/platform/commit/bd968fa)) + + + + +## 4.0.2 (2017-08-02) ### Bug Fixes +* **createSelector:** memoize projector function (#228) ([e2f1e57](https://github.com/ngrx/platform/commit/e2f1e57)), closes [#226](https://github.com/ngrx/platform/issues/226) * **Devtools:** Removed SHOULD_INSTRUMENT token used to eagerly inject providers (#57) ([b90df34](https://github.com/ngrx/platform/commit/b90df34)) * **omit:** Strengthen the type checking of the omit utility function ([3982038](https://github.com/ngrx/platform/commit/3982038)) * **Store:** Exported initial state tokens (#65) ([4b27b6d](https://github.com/ngrx/platform/commit/4b27b6d)) * **Store:** pass all required arguments to projector (#74) ([9b82b3a](https://github.com/ngrx/platform/commit/9b82b3a)) +* **Store:** Remove auto-memoization of selector functions ([90899f7](https://github.com/ngrx/platform/commit/90899f7)), closes [#118](https://github.com/ngrx/platform/issues/118) * **Store:** Remove parameter destructuring for strict mode (#33) (#77) ([c9d6a45](https://github.com/ngrx/platform/commit/c9d6a45)) * **Store:** Removed readonly from type (#72) ([68274c9](https://github.com/ngrx/platform/commit/68274c9)) @@ -27,6 +43,8 @@ See [standard-version](https://github.com/conventional-changelog/standard-versio * **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) * **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) * **store:** Add 'createSelector' and 'createFeatureSelector' utils (#10) ([41758b1](https://github.com/ngrx/platform/commit/41758b1)) +* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) +* **Store:** Added initial state function support for features. Added more tests (#85) ([5e5d7dd](https://github.com/ngrx/platform/commit/5e5d7dd)) * **Store:** Allow initial state function for AoT compatibility (#59) ([1a166ec](https://github.com/ngrx/platform/commit/1a166ec)), closes [#51](https://github.com/ngrx/platform/issues/51) * **Store:** Allow parent modules to provide reducers with tokens (#36) ([069b12f](https://github.com/ngrx/platform/commit/069b12f)), closes [#34](https://github.com/ngrx/platform/issues/34) * **Store:** Simplify API for adding meta-reducers (#87) ([d2295c7](https://github.com/ngrx/platform/commit/d2295c7)) @@ -71,3 +89,75 @@ imports: [ }) export class SomeFeatureModule { } ``` + + + + + +# 4.0.0 (2017-07-18) + + +### Bug Fixes + +* **Devtools:** Removed SHOULD_INSTRUMENT token used to eagerly inject providers (#57) ([b90df34](https://github.com/ngrx/platform/commit/b90df34)) +* **omit:** Strengthen the type checking of the omit utility function ([3982038](https://github.com/ngrx/platform/commit/3982038)) +* **Store:** Exported initial state tokens (#65) ([4b27b6d](https://github.com/ngrx/platform/commit/4b27b6d)) +* **Store:** pass all required arguments to projector (#74) ([9b82b3a](https://github.com/ngrx/platform/commit/9b82b3a)) +* **Store:** Remove parameter destructuring for strict mode (#33) (#77) ([c9d6a45](https://github.com/ngrx/platform/commit/c9d6a45)) +* **Store:** Removed readonly from type (#72) ([68274c9](https://github.com/ngrx/platform/commit/68274c9)) + + +### Code Refactoring + +* **Effects:** Simplified AP, added better error reporting and effects stream control ([015107f](https://github.com/ngrx/platform/commit/015107f)) + + +### Features + +* **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) +* **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) +* **store:** Add 'createSelector' and 'createFeatureSelector' utils (#10) ([41758b1](https://github.com/ngrx/platform/commit/41758b1)) +* **Store:** Allow initial state function for AoT compatibility (#59) ([1a166ec](https://github.com/ngrx/platform/commit/1a166ec)), closes [#51](https://github.com/ngrx/platform/issues/51) +* **Store:** Allow parent modules to provide reducers with tokens (#36) ([069b12f](https://github.com/ngrx/platform/commit/069b12f)), closes [#34](https://github.com/ngrx/platform/issues/34) +* **Store:** Simplify API for adding meta-reducers (#87) ([d2295c7](https://github.com/ngrx/platform/commit/d2295c7)) + + +### BREAKING CHANGES + +* **Effects:** Effects API for registering effects has been updated to allow for multiple classes to be provided. + +BEFORE: +```ts +@NgModule({ +imports: [ +EffectsModule.run(SourceA), +EffectsModule.run(SourceB) +] +}) +export class AppModule { } +``` + +AFTER: +```ts +@NgModule({ +imports: [ +EffectsModule.forRoot([ + SourceA, + SourceB, + SourceC, +]) +] +}) +export class AppModule { } + +@NgModule({ +imports: [ +EffectsModule.forFeature([ + FeatureSourceA, + FeatureSourceB, + FeatureSourceC, +]) +] +}) +export class SomeFeatureModule { } +``` diff --git a/modules/store/package.json b/modules/store/package.json index a71b05f592..76b1b7793c 100644 --- a/modules/store/package.json +++ b/modules/store/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/store", - "version": "4.0.2", + "version": "4.0.3", "description": "RxJS powered Redux for Angular apps", "module": "@ngrx/store.es5.js", "es2015": "@ngrx/store.js", From b42ba975e810993c50528818cff290caf5fce18c Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 16 Aug 2017 14:07:17 -0500 Subject: [PATCH 27/67] docs(Changelog): Point changelog to individual package changelogs (#282) --- CHANGELOG.md | 122 +++------------------------------------------------ 1 file changed, 5 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61b472a5a..5b69b3f236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,118 +1,6 @@ - -# [4.0.0](https://github.com/ngrx/platform/compare/v4.0.2...v4.0.0) (2017-08-02) - - - - -## [4.0.2](https://github.com/ngrx/platform/compare/v4.0.1...v4.0.2) (2017-08-02) - - -### Bug Fixes - -* **createSelector:** memoize projector function (#228) ([e2f1e57](https://github.com/ngrx/platform/commit/e2f1e57)), closes [#226](https://github.com/ngrx/platform/issues/226) -* **Effects:** Wrap testing source in an Actions observable (#121) ([bfdb83b](https://github.com/ngrx/platform/commit/bfdb83b)), closes [#117](https://github.com/ngrx/platform/issues/117) -* **RouterStore:** Add support for cancellation with CanLoad guard (#223) ([2c006e8](https://github.com/ngrx/platform/commit/2c006e8)), closes [#213](https://github.com/ngrx/platform/issues/213) -* **Store:** Remove auto-memoization of selector functions ([90899f7](https://github.com/ngrx/platform/commit/90899f7)), closes [#118](https://github.com/ngrx/platform/issues/118) - - -### Features - -* **Effects:** Add generic type to the "ofType" operator ([55c13b2](https://github.com/ngrx/platform/commit/55c13b2)) -* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) -* **Store:** Added initial state function support for features. Added more tests (#85) ([5e5d7dd](https://github.com/ngrx/platform/commit/5e5d7dd)) - - - - -## [4.0.1](https://github.com/ngrx/platform/compare/v4.0.0...v4.0.1) (2017-07-18) - - -### Bug Fixes - -* **effects:** allow downleveled annotations (#98) ([875b326](https://github.com/ngrx/platform/commit/875b326)), closes [#93](https://github.com/ngrx/platform/issues/93) -* **effects:** make correct export path for testing module (#96) ([a5aad22](https://github.com/ngrx/platform/commit/a5aad22)), closes [#94](https://github.com/ngrx/platform/issues/94) - - - - -# [4.0.0](https://github.com/ngrx/platform/compare/68bd9df...v4.0.0) (2017-07-18) - - -### Bug Fixes - -* **build:** Fixed deployment of latest master as commit (#18) ([5d0ecf9](https://github.com/ngrx/platform/commit/5d0ecf9)), closes [#18](https://github.com/ngrx/platform/issues/18) -* **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) -* **build:** Limit concurrency for lerna bootstrap ([7e7a7d8](https://github.com/ngrx/platform/commit/7e7a7d8)) -* **Devtools:** Removed SHOULD_INSTRUMENT token used to eagerly inject providers (#57) ([b90df34](https://github.com/ngrx/platform/commit/b90df34)) -* **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) -* **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) -* **Example:** Fix Book State interface parent (#90) ([6982952](https://github.com/ngrx/platform/commit/6982952)), closes [#90](https://github.com/ngrx/platform/issues/90) -* **example-app:** Suppress StoreDevtoolsConfig compiler warning ([8804156](https://github.com/ngrx/platform/commit/8804156)) -* **omit:** Strengthen the type checking of the omit utility function ([3982038](https://github.com/ngrx/platform/commit/3982038)) -* **router-store:** NavigationCancel and NavigationError creates a cycle when used with routerReducer ([a085730](https://github.com/ngrx/platform/commit/a085730)), closes [#68](https://github.com/ngrx/platform/issues/68) -* **Store:** Exported initial state tokens (#65) ([4b27b6d](https://github.com/ngrx/platform/commit/4b27b6d)) -* **Store:** pass all required arguments to projector (#74) ([9b82b3a](https://github.com/ngrx/platform/commit/9b82b3a)) -* **Store:** Remove parameter destructuring for strict mode (#33) (#77) ([c9d6a45](https://github.com/ngrx/platform/commit/c9d6a45)) -* **Store:** Removed readonly from type (#72) ([68274c9](https://github.com/ngrx/platform/commit/68274c9)) -* **StoreDevtools:** Type InjectionToken for AOT compilation ([e21d688](https://github.com/ngrx/platform/commit/e21d688)) - - -### Code Refactoring - -* **Effects:** Simplified AP, added better error reporting and effects stream control ([015107f](https://github.com/ngrx/platform/commit/015107f)) - - -### Features - -* **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) -* **Effects:** Ensure effects are only subscribed to once ([089abdc](https://github.com/ngrx/platform/commit/089abdc)) -* **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) -* **router-store:** Added action types (#47) ([1f67cb3](https://github.com/ngrx/platform/commit/1f67cb3)), closes [#44](https://github.com/ngrx/platform/issues/44) -* **store:** Add 'createSelector' and 'createFeatureSelector' utils (#10) ([41758b1](https://github.com/ngrx/platform/commit/41758b1)) -* **Store:** Allow initial state function for AoT compatibility (#59) ([1a166ec](https://github.com/ngrx/platform/commit/1a166ec)), closes [#51](https://github.com/ngrx/platform/issues/51) -* **Store:** Allow parent modules to provide reducers with tokens (#36) ([069b12f](https://github.com/ngrx/platform/commit/069b12f)), closes [#34](https://github.com/ngrx/platform/issues/34) -* **Store:** Simplify API for adding meta-reducers (#87) ([d2295c7](https://github.com/ngrx/platform/commit/d2295c7)) - - -### BREAKING CHANGES - -* **Effects:** Effects API for registering effects has been updated to allow for multiple classes to be provided. - -BEFORE: -```ts -@NgModule({ - imports: [ - EffectsModule.run(SourceA), - EffectsModule.run(SourceB) - ] -}) -export class AppModule { } -``` - -AFTER: -```ts -@NgModule({ - imports: [ - EffectsModule.forRoot([ - SourceA, - SourceB, - SourceC, - ]) - ] -}) -export class AppModule { } - -@NgModule({ - imports: [ - EffectsModule.forFeature([ - FeatureSourceA, - FeatureSourceB, - FeatureSourceC, - ]) - ] -}) -export class SomeFeatureModule { } -``` - - +# Changelogs +- [@ngrx/store](./modules/store/CHANGELOG.md) +- [@ngrx/effects](./modules/effects/CHANGELOG.md) +- [@ngrx/router-store](./modules/router-store/CHANGELOG.md) +- [@ngrx/store-devtools](./modules/store-devtools/CHANGELOG.md) From 090462a2c9998983bda9bb50ac7668a9d6c14b0d Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 16 Aug 2017 21:04:21 -0500 Subject: [PATCH 28/67] docs: Add more information on contributing and API docs (#283) Closes #263, #274, #279 --- CONTRIBUTING.md | 28 ++++++++++++++++--- README.md | 3 ++ docs/store/api.md | 16 +++++++---- docs/store/selectors.md | 2 +- example-app/README.md | 5 +++- .../auth/components/login-form.component.ts | 4 +-- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c42b42ed5e..915a5e3e8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,26 +6,46 @@ npm install ``` +OR +``` +yarn +``` + ### Testing ``` npm test ``` +OR + +``` +yarn test +``` + ## Submitting Pull Requests **Please follow these basic steps to simplify pull request reviews - if you don't you'll probably just be asked to anyway.** * Please rebase your branch against the current master -* Run ```npm install``` to make sure your development dependencies are up-to-date +* Run the `Setup` command to make sure your development dependencies are up-to-date * Please ensure the test suite passes before submitting a PR * If you've added new functionality, **please** include tests which validate its behavior * Make reference to possible [issues](https://github.com/ngrx/platform/issues) on PR comment ## Submitting bug reports -* Please detail the affected browser(s) and operating system(s) -* Please be sure to state which version of node **and** npm you're using +* Search through issues to see if a previous issue has already been reported and/or fixed. +* Provide a _small_ reproduction using a [plunker template](http://plnkr.co/edit/tpl:757r6L?p=preview) or github repo. +* Please detail the affected browser(s) and operating system(s). +* Please be sure to state which version of Angular, node and npm you're using. + +## Submitting New features + +* We value keeping the API surface small and concise, which factors into whether new features are accepted. +* Submit an issue with the prefix `RFC: ` with your feature request. +* The feature will be discussed and considered. +* Once the PR is submitted, it will be reviewed and merged once approved. ## Financial contributions @@ -63,4 +83,4 @@ Thank you to all our sponsors! (please ask your company to also support this ope - \ No newline at end of file + diff --git a/README.md b/README.md index b5f31f67b8..8c14414609 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Reactive libraries for Angular - [@ngrx/store-devtools](./docs/store-devtools/README.md) - Store instrumentation that enables a [powerful time-travelling debugger](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en). +## Examples +- [example-app](./example-app/README.md) - Example application utilizing @ngrx libraries, showcasing common patterns and best practices. + ## Migration - [Migration guide](./MIGRATION.md) for users of ngrx packages prior to 4.x. diff --git a/docs/store/api.md b/docs/store/api.md index 7d7758c0ec..b908e12713 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -49,7 +49,7 @@ export function getInitialState() { configuration option to provide an array of meta-reducers that are composed from right to left. ```ts -import { StoreModule, ActionReducer } from '@ngrx/store'; +import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store'; import { reducers } from './reducers'; // console.log all actions @@ -62,7 +62,7 @@ export function debug(reducer: ActionReducer): ActionReducer { } } -export const metaReducers = [debug]; +export const metaReducers: MetaReducer = [debug]; @NgModule({ imports: [ @@ -81,17 +81,23 @@ and `metaReducers` configuration options are available. ```ts // feature.module.ts -import { StoreModule } from '@ngrx/store'; -import { reducers } from './reducers'; +import { StoreModule, ActionReducerMap } from '@ngrx/store'; + +export const reducers: ActionReducerMap = { + subFeatureA: featureAReducer, + subFeatureB: featureBReducer, +}; @NgModule({ imports: [ - StoreModule.forFeature('featureName', reducers, { }) + StoreModule.forFeature('featureName', reducers) ] }) export class FeatureModule {} ``` +The feature state is added to the global application state once the feature is loaded. The feature state can then be selected using the [./selectors.md#createFeatureSelector](createFeatureSelector) convenience method. + ## Injecting Reducers To inject the root reducers into your application, use an `InjectionToken` and a `Provider` to register the reducers through dependency injection. diff --git a/docs/store/selectors.md b/docs/store/selectors.md index 88ce664417..322c7b6f38 100644 --- a/docs/store/selectors.md +++ b/docs/store/selectors.md @@ -51,7 +51,7 @@ class MyAppComponent { ## createFeatureSelector -The `createFeatureSelector` methods returns a selector function for a feature slice of state. +The `createFeatureSelector` is a convenience method for returning a top level feature state. It returns a typed selector function for a feature slice of state. ### Example diff --git a/example-app/README.md b/example-app/README.md index e4b13a61cd..ed59e3ee39 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -32,7 +32,10 @@ npm install yarn # start the server -npm run example:start +npm run build && npm run cli -- serve + +# OR +yarn run example:start ``` Navigate to [http://localhost:4200/](http://localhost:4200/) in your browser diff --git a/example-app/app/auth/components/login-form.component.ts b/example-app/app/auth/components/login-form.component.ts index 8023b2f516..dd99ec77ec 100644 --- a/example-app/app/auth/components/login-form.component.ts +++ b/example-app/app/auth/components/login-form.component.ts @@ -71,9 +71,9 @@ export class LoginFormComponent implements OnInit { set pending(isPending: boolean) { if (isPending) { this.form.disable(); + } else { + this.form.enable(); } - - this.form.enable(); } @Input() errorMessage: string | null; From 78772b5035589478f55ea663a2ce37af51e440f2 Mon Sep 17 00:00:00 2001 From: Sharikov Vladislav Date: Thu, 17 Aug 2017 15:50:28 +0300 Subject: [PATCH 29/67] chore(docs): Add missing Store import (#285) --- docs/store/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/store/testing.md b/docs/store/testing.md index 4446494727..996c57df9a 100644 --- a/docs/store/testing.md +++ b/docs/store/testing.md @@ -40,7 +40,7 @@ export class MyComponent implements OnInit { my-component.spec.ts ```ts import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { StoreModule, combineReducers } from '@ngrx/store'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { MyComponent } from './my.component'; import * as fromRoot from '../reducers'; import * as fromFeature from './reducers'; From bbb7c99ff4fb31ad74a332df6a837676261b6abf Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 17 Aug 2017 08:32:53 -0500 Subject: [PATCH 30/67] fix(RouterStore): Only serialize snapshot in preactivation hook (#287) Closes #286 --- modules/router-store/src/router_store_module.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 000ab7608a..424f6b275d 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -225,12 +225,12 @@ export class StoreRouterConnectingModule { private dispatchRouterNavigation(): void { this.dispatchRouterAction(ROUTER_NAVIGATION, { routerState: this.routerState, - event: { - id: this.lastRoutesRecognized.id, - url: this.lastRoutesRecognized.url, - urlAfterRedirects: this.lastRoutesRecognized.urlAfterRedirects, - state: this.serializer.serialize(this.routerState), - } as RoutesRecognized, + event: new RoutesRecognized( + this.lastRoutesRecognized.id, + this.lastRoutesRecognized.url, + this.lastRoutesRecognized.urlAfterRedirects, + this.routerState + ), }); } From bf7f70cbb535b3eb62691d608a5870284fb52ebb Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 17 Aug 2017 08:40:03 -0500 Subject: [PATCH 31/67] fix(Effects): Use factory provide for console (#288) Closes #276 --- modules/effects/src/effects_module.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/effects/src/effects_module.ts b/modules/effects/src/effects_module.ts index 32c5aafa13..90d09e1b9e 100644 --- a/modules/effects/src/effects_module.ts +++ b/modules/effects/src/effects_module.ts @@ -40,7 +40,7 @@ export class EffectsModule { }, { provide: CONSOLE, - useValue: console, + useFactory: getConsole, }, ], }; @@ -50,3 +50,7 @@ export class EffectsModule { export function createSourceInstances(...instances: any[]) { return instances; } + +export function getConsole() { + return console; +} From 6da3ec50478a9659bd68e7d996eaef8390f1ce4e Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 17 Aug 2017 18:31:13 -0500 Subject: [PATCH 32/67] fix(RouterStore): Add generic type to RouterReducerState (#292) Closes #289 --- docs/router-store/api.md | 22 +++++++++--- example-app/app/reducers/index.ts | 3 +- example-app/app/shared/utils.ts | 10 +++--- modules/router-store/spec/integration.spec.ts | 15 ++++++-- .../router-store/src/router_store_module.ts | 34 +++++++++---------- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/docs/router-store/api.md b/docs/router-store/api.md index b9eca6d52e..052b9b58f3 100644 --- a/docs/router-store/api.md +++ b/docs/router-store/api.md @@ -8,31 +8,43 @@ issues when used with the Store Devtools. In most cases, you may only need a pie To use the time-traveling debugging in the Devtools, you must return an object containing the `url` when using the `routerReducer`. ```ts -import { StoreModule } from '@ngrx/store'; +import { StoreModule, ActionReducerMap } from '@ngrx/store'; +import { Params } from '@angular/router'; import { StoreRouterConnectingModule, routerReducer, + RouterReducerState, RouterStateSerializer, - RouterStateSnapshotType + RouterStateSnapshot } from '@ngrx/router-store'; export interface RouterStateUrl { url: string; + queryParams: Params; +} + +export interface State { + routerReducer: RouterReducerState; } export class CustomSerializer implements RouterStateSerializer { serialize(routerState: RouterStateSnapshot): RouterStateUrl { const { url } = routerState; + const queryParams = routerState.root.queryParams; - // Only return an object including the URL + // Only return an object including the URL and query params // instead of the entire snapshot - return { url }; + return { url, queryParams }; } } +export const reducers: ActionReducerMap = { + routerReducer: routerReducer +}; + @NgModule({ imports: [ - StoreModule.forRoot({ routerReducer: routerReducer }), + StoreModule.forRoot(reducers), RouterModule.forRoot([ // routes ]), diff --git a/example-app/app/reducers/index.ts b/example-app/app/reducers/index.ts index cb26504f4a..86b13db736 100644 --- a/example-app/app/reducers/index.ts +++ b/example-app/app/reducers/index.ts @@ -6,6 +6,7 @@ import { MetaReducer, } from '@ngrx/store'; import { environment } from '../../environments/environment'; +import { RouterStateUrl } from '../shared/utils'; import * as fromRouter from '@ngrx/router-store'; /** @@ -23,7 +24,7 @@ import * as fromLayout from '../core/reducers/layout'; */ export interface State { layout: fromLayout.State; - routerReducer: fromRouter.RouterReducerState; + routerReducer: fromRouter.RouterReducerState; } /** diff --git a/example-app/app/shared/utils.ts b/example-app/app/shared/utils.ts index 758011bce2..c292c84780 100644 --- a/example-app/app/shared/utils.ts +++ b/example-app/app/shared/utils.ts @@ -1,24 +1,26 @@ import { RouterStateSerializer } from '@ngrx/router-store'; -import { RouterStateSnapshot } from '@angular/router'; +import { RouterStateSnapshot, Params } from '@angular/router'; /** * The RouterStateSerializer takes the current RouterStateSnapshot * and returns any pertinent information needed. The snapshot contains * all information about the state of the router at the given point in time. * The entire snapshot is complex and not always needed. In this case, you only - * need the URL from the snapshot in the store. Other items could be - * returned such as route parameters, query parameters and static route data. + * need the URL and query parameters from the snapshot in the store. Other items could be + * returned such as route parameters and static route data. */ export interface RouterStateUrl { url: string; + queryParams: Params; } export class CustomRouterStateSerializer implements RouterStateSerializer { serialize(routerState: RouterStateSnapshot): RouterStateUrl { const { url } = routerState; + const queryParams = routerState.root.queryParams; - return { url }; + return { url, queryParams }; } } diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index c0c53184a0..4af94dff08 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -325,11 +325,13 @@ describe('integration spec', () => { : null; }; - class CustomSerializer implements RouterStateSerializer<{ url: string }> { + class CustomSerializer + implements RouterStateSerializer<{ url: string; params: any }> { serialize(routerState: RouterStateSnapshot) { const url = `${routerState.url}-custom`; + const params = { test: 1 }; - return { url }; + return { url, params }; } } @@ -353,7 +355,14 @@ describe('integration spec', () => { expect(log).toEqual([ { type: 'router', event: 'NavigationStart', url: '/next' }, { type: 'router', event: 'RoutesRecognized', url: '/next' }, - { type: 'store', state: { url: '/next-custom', navigationId: 2 } }, + { + type: 'store', + state: { + url: '/next-custom', + navigationId: 2, + params: { test: 1 }, + }, + }, { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); log.splice(0); diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 424f6b275d..cbf111c2be 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -28,7 +28,7 @@ export type RouterNavigationPayload = { /** * An action dispatched when the router navigates. */ -export type RouterNavigationAction = { +export type RouterNavigationAction = { type: typeof ROUTER_NAVIGATION; payload: RouterNavigationPayload; }; @@ -41,8 +41,8 @@ export const ROUTER_CANCEL = 'ROUTER_CANCEL'; /** * Payload of ROUTER_CANCEL. */ -export type RouterCancelPayload = { - routerState: RouterStateSnapshot; +export type RouterCancelPayload = { + routerState: V; storeState: T; event: NavigationCancel; }; @@ -50,9 +50,9 @@ export type RouterCancelPayload = { /** * An action dispatched when the router cancel navigation. */ -export type RouterCancelAction = { +export type RouterCancelAction = { type: typeof ROUTER_CANCEL; - payload: RouterCancelPayload; + payload: RouterCancelPayload; }; /** @@ -63,8 +63,8 @@ export const ROUTER_ERROR = 'ROUTE_ERROR'; /** * Payload of ROUTER_ERROR. */ -export type RouterErrorPayload = { - routerState: RouterStateSnapshot; +export type RouterErrorPayload = { + routerState: V; storeState: T; event: NavigationError; }; @@ -72,28 +72,28 @@ export type RouterErrorPayload = { /** * An action dispatched when the router errors. */ -export type RouterErrorAction = { +export type RouterErrorAction = { type: typeof ROUTER_ERROR; - payload: RouterErrorPayload; + payload: RouterErrorPayload; }; /** * An union type of router actions. */ -export type RouterAction = +export type RouterAction = | RouterNavigationAction - | RouterCancelAction - | RouterErrorAction; + | RouterCancelAction + | RouterErrorAction; -export type RouterReducerState = { - state: RouterStateSnapshot; +export type RouterReducerState = { + state: T; navigationId: number; }; -export function routerReducer( - state: RouterReducerState, +export function routerReducer( + state: RouterReducerState, action: RouterAction -): RouterReducerState { +): RouterReducerState { switch (action.type) { case ROUTER_NAVIGATION: case ROUTER_ERROR: From bec84a83c6f38f00e660c79e8a0927f7f9c78b6f Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 17 Aug 2017 18:34:06 -0500 Subject: [PATCH 33/67] build: Add Release command for tagging builds --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 967636461e..984a0f6dbc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "prettier": "prettier --parser typescript --single-quote --trailing-comma --write \"./**/*.ts\"", "watch:tests": "chokidar 'modules/**/*.ts' --initial -c 'nyc --reporter=text --reporter=html yarn run test:unit'", "postinstall": "opencollective postinstall", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", + "release": "lerna publish --skip-npm --conventional-commits && npm run build" }, "keywords": [ "ngrx", From bdae25f5e095df02e79fb5d21cc538d2309f1dad Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 17 Aug 2017 18:34:36 -0500 Subject: [PATCH 34/67] v4.0.4 --- lerna.json | 2 +- modules/effects/CHANGELOG.md | 111 +++++++++++++++++++++++++++++- modules/effects/package.json | 2 +- modules/router-store/CHANGELOG.md | 38 ++++++++++ modules/router-store/package.json | 2 +- 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/lerna.json b/lerna.json index ce3400f414..7e11594192 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "modules/*" ], - "version": "4.0.3", + "version": "4.0.4", "npmClient": "yarn" } diff --git a/modules/effects/CHANGELOG.md b/modules/effects/CHANGELOG.md index 4b6da68156..38ffce669a 100644 --- a/modules/effects/CHANGELOG.md +++ b/modules/effects/CHANGELOG.md @@ -3,7 +3,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - + +## 4.0.4 (2017-08-17) + + +### Bug Fixes + +* **Effects:** Use factory provide for console (#288) ([bf7f70c](https://github.com/ngrx/platform/commit/bf7f70c)), closes [#276](https://github.com/ngrx/platform/issues/276) + + + + ## 4.0.3 (2017-08-16) @@ -98,13 +108,42 @@ export class SomeFeatureModule { } - -# 4.0.0 (2017-07-18) + +## 4.0.3 (2017-08-16) + + +### Bug Fixes + +* **Effects:** Deprecate toPayload utility function (#266) ([1cbb2c9](https://github.com/ngrx/platform/commit/1cbb2c9)) +* **Effects:** Ensure StoreModule is loaded before effects (#230) ([065d33e](https://github.com/ngrx/platform/commit/065d33e)), closes [#184](https://github.com/ngrx/platform/issues/184) [#219](https://github.com/ngrx/platform/issues/219) +* **Effects:** Export EffectsNotification interface (#231) ([2b1a076](https://github.com/ngrx/platform/commit/2b1a076)) + + + + +## 4.0.2 (2017-08-02) + + +### Bug Fixes + +* **Effects:** Wrap testing source in an Actions observable (#121) ([bfdb83b](https://github.com/ngrx/platform/commit/bfdb83b)), closes [#117](https://github.com/ngrx/platform/issues/117) + + +### Features + +* **Effects:** Add generic type to the "ofType" operator ([55c13b2](https://github.com/ngrx/platform/commit/55c13b2)) +* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) + + + + +## 4.0.1 (2017-07-18) ### Bug Fixes * **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) +* **effects:** allow downleveled annotations (#98) ([875b326](https://github.com/ngrx/platform/commit/875b326)), closes [#93](https://github.com/ngrx/platform/issues/93) * **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) * **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) @@ -160,3 +199,69 @@ EffectsModule.forFeature([ }) export class SomeFeatureModule { } ``` + + + + + +# 4.0.0 (2017-07-18) + + +### Bug Fixes + +* **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) +* **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) +* **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) + + +### Code Refactoring + +* **Effects:** Simplified AP, added better error reporting and effects stream control ([015107f](https://github.com/ngrx/platform/commit/015107f)) + + +### Features + +* **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) +* **Effects:** Ensure effects are only subscribed to once ([089abdc](https://github.com/ngrx/platform/commit/089abdc)) +* **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) + + +### BREAKING CHANGES + +* **Effects:** Effects API for registering effects has been updated to allow for multiple classes to be provided. + +BEFORE: +```ts +@NgModule({ +imports: [ +EffectsModule.run(SourceA), +EffectsModule.run(SourceB) +] +}) +export class AppModule { } +``` + +AFTER: +```ts +@NgModule({ +imports: [ +EffectsModule.forRoot([ +SourceA, +SourceB, +SourceC, +]) +] +}) +export class AppModule { } + +@NgModule({ +imports: [ +EffectsModule.forFeature([ +FeatureSourceA, +FeatureSourceB, +FeatureSourceC, +]) +] +}) +export class SomeFeatureModule { } +``` diff --git a/modules/effects/package.json b/modules/effects/package.json index 831a57b61a..2777686d33 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/effects", - "version": "4.0.3", + "version": "4.0.4", "description": "Side effect model for @ngrx/store", "module": "@ngrx/effects.es5.js", "es2015": "@ngrx/effects.js", diff --git a/modules/router-store/CHANGELOG.md b/modules/router-store/CHANGELOG.md index d32602e733..9a5aeedef6 100644 --- a/modules/router-store/CHANGELOG.md +++ b/modules/router-store/CHANGELOG.md @@ -3,6 +3,44 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## 4.0.4 (2017-08-17) + + +### Bug Fixes + +* **RouterStore:** Add generic type to RouterReducerState (#292) ([6da3ec5](https://github.com/ngrx/platform/commit/6da3ec5)), closes [#289](https://github.com/ngrx/platform/issues/289) +* **RouterStore:** Only serialize snapshot in preactivation hook (#287) ([bbb7c99](https://github.com/ngrx/platform/commit/bbb7c99)), closes [#286](https://github.com/ngrx/platform/issues/286) + + + + +## 4.0.3 (2017-08-16) + + +### Features + +* **RouterStore:** Add serializer for router state snapshot (#188) ([0fc1bcc](https://github.com/ngrx/platform/commit/0fc1bcc)), closes [#97](https://github.com/ngrx/platform/issues/97) [#104](https://github.com/ngrx/platform/issues/104) [#237](https://github.com/ngrx/platform/issues/237) + + + + +## 4.0.2 (2017-08-02) + + +### Bug Fixes + +* **router-store:** NavigationCancel and NavigationError creates a cycle when used with routerReducer ([a085730](https://github.com/ngrx/platform/commit/a085730)), closes [#68](https://github.com/ngrx/platform/issues/68) +* **RouterStore:** Add support for cancellation with CanLoad guard (#223) ([2c006e8](https://github.com/ngrx/platform/commit/2c006e8)), closes [#213](https://github.com/ngrx/platform/issues/213) + + +### Features + +* **router-store:** Added action types (#47) ([1f67cb3](https://github.com/ngrx/platform/commit/1f67cb3)), closes [#44](https://github.com/ngrx/platform/issues/44) + + + + ## 4.0.3 (2017-08-16) diff --git a/modules/router-store/package.json b/modules/router-store/package.json index 1bd5557dbe..052251bc35 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/router-store", - "version": "4.0.3", + "version": "4.0.4", "description": "Bindings to connect @angular/router to @ngrx/store", "module": "@ngrx/router-store.es5.js", "es2015": "@ngrx/router-store.js", From 335d255fdc55d68d0ae3b8ed587788aab81a6d30 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 17 Aug 2017 19:47:08 -0500 Subject: [PATCH 35/67] fix(Entity): Simplify target index finder for sorted entities --- modules/entity/src/sorted_state_adapter.ts | 32 ++++++---------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/modules/entity/src/sorted_state_adapter.ts b/modules/entity/src/sorted_state_adapter.ts index 53bdcca992..44a4afd269 100644 --- a/modules/entity/src/sorted_state_adapter.ts +++ b/modules/entity/src/sorted_state_adapter.ts @@ -88,35 +88,21 @@ export function createSortedStateAdapter( } } - function findTargetIndex( - state: R, - model: T, - left = 0, - right = state.ids.length - 1 - ) { - if (right === -1) { + function findTargetIndex(state: R, model: T) { + if (state.ids.length === 0) { return 0; } - let middle: number; + for (let i = 0; i < state.ids.length; i++) { + const entity = state.entities[state.ids[i]]; + const isSmaller = sort(model, entity) < 0; - while (true) { - middle = Math.floor((left + right) / 2); - - const result = sort(state.entities[state.ids[middle]], model); - - if (result === 0) { - return middle; - } else if (result < 0) { - left = middle + 1; - } else { - right = middle - 1; - } - - if (left > right) { - return state.ids.length - 1; + if (isSmaller) { + return i; } } + + return state.ids.length - 1; } return { From fbd6a66ac7aaf73c5293ee5e5299313e4b0f7d96 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 17 Aug 2017 19:50:19 -0500 Subject: [PATCH 36/67] fix(Entity): Return a referentially equal state if state did not change --- .../entity/spec/sorted_state_adapter.spec.ts | 4 +- .../spec/unsorted_state_adapter.spec.ts | 4 +- modules/entity/src/sorted_state_adapter.ts | 43 ++++++++++------ modules/entity/src/state_adapter.ts | 10 ++-- modules/entity/src/unsorted_state_adapter.ts | 49 +++++++++++++------ 5 files changed, 73 insertions(+), 37 deletions(-) diff --git a/modules/entity/spec/sorted_state_adapter.spec.ts b/modules/entity/spec/sorted_state_adapter.spec.ts index eaef6dc797..f8b8d7ce5d 100644 --- a/modules/entity/spec/sorted_state_adapter.spec.ts +++ b/modules/entity/spec/sorted_state_adapter.spec.ts @@ -36,7 +36,7 @@ describe('Sorted State Adapter', () => { const readded = adapter.addOne(TheGreatGatsby, withOneEntity); - expect(readded).toEqual(withOneEntity); + expect(readded).toBe(withOneEntity); }); it('should let you add many entities to the state', () => { @@ -150,7 +150,7 @@ describe('Sorted State Adapter', () => { state ); - expect(withUpdates).toEqual(state); + expect(withUpdates).toBe(state); }); it('should let you update the id of entity', () => { diff --git a/modules/entity/spec/unsorted_state_adapter.spec.ts b/modules/entity/spec/unsorted_state_adapter.spec.ts index 2a9d1176a8..941ab87088 100644 --- a/modules/entity/spec/unsorted_state_adapter.spec.ts +++ b/modules/entity/spec/unsorted_state_adapter.spec.ts @@ -35,7 +35,7 @@ describe('Unsorted State Adapter', () => { const readded = adapter.addOne(TheGreatGatsby, withOneEntity); - expect(readded).toEqual(withOneEntity); + expect(readded).toBe(withOneEntity); }); it('should let you add many entities to the state', () => { @@ -149,7 +149,7 @@ describe('Unsorted State Adapter', () => { state ); - expect(withUpdates).toEqual(state); + expect(withUpdates).toBe(state); }); it('should let you update the id of entity', () => { diff --git a/modules/entity/src/sorted_state_adapter.ts b/modules/entity/src/sorted_state_adapter.ts index 44a4afd269..13ece9f8e5 100644 --- a/modules/entity/src/sorted_state_adapter.ts +++ b/modules/entity/src/sorted_state_adapter.ts @@ -19,26 +19,31 @@ export function createSortedStateAdapter( selectId ); - function addOneMutably(entity: T, state: R): void { + function addOneMutably(entity: T, state: R): boolean { const key = selectId(entity); - const index = state.ids.indexOf(key); - if (index !== -1) { - return; + if (key in state.entities) { + return false; } const insertAt = findTargetIndex(state, entity); state.ids.splice(insertAt, 0, key); state.entities[key] = entity; + + return true; } - function addManyMutably(newModels: T[], state: R): void { + function addManyMutably(newModels: T[], state: R): boolean { + let didMutate = false; + for (let index in newModels) { - addOneMutably(newModels[index], state); + didMutate = addOneMutably(newModels[index], state) || didMutate; } + + return didMutate; } - function addAllMutably(models: T[], state: R): void { + function addAllMutably(models: T[], state: R): boolean { const sortedModels = models.sort(sort); state.entities = {}; @@ -47,13 +52,13 @@ export function createSortedStateAdapter( state.entities[id] = model; return id; }); - } - function updateOneMutably(update: Update, state: R): void { - const index = state.ids.indexOf(update.id); + return true; + } - if (index === -1) { - return; + function updateOneMutably(update: Update, state: R): boolean { + if (!(update.id in state.entities)) { + return false; } const original = state.entities[update.id]; @@ -64,14 +69,16 @@ export function createSortedStateAdapter( if (result === 0) { if (updatedKey !== update.id) { delete state.entities[update.id]; + const index = state.ids.indexOf(update.id); state.ids[index] = updatedKey; } state.entities[updatedKey] = updated; - return; + return true; } + const index = state.ids.indexOf(update.id); state.ids.splice(index, 1); state.ids.splice(findTargetIndex(state, updated), 0, updatedKey); @@ -80,12 +87,18 @@ export function createSortedStateAdapter( } state.entities[updatedKey] = updated; + + return true; } - function updateManyMutably(updates: Update[], state: R): void { + function updateManyMutably(updates: Update[], state: R): boolean { + let didMutate = false; + for (let index in updates) { - updateOneMutably(updates[index], state); + didMutate = updateOneMutably(updates[index], state) || didMutate; } + + return didMutate; } function findTargetIndex(state: R, model: T) { diff --git a/modules/entity/src/state_adapter.ts b/modules/entity/src/state_adapter.ts index 6a6b24f744..9a0d6c68be 100644 --- a/modules/entity/src/state_adapter.ts +++ b/modules/entity/src/state_adapter.ts @@ -1,7 +1,7 @@ import { EntityState, EntityStateAdapter } from './models'; export function createStateOperator( - mutator: (arg: R, state: EntityState) => void + mutator: (arg: R, state: EntityState) => boolean ) { return function operation>(arg: R, state: S): S { const clonedEntityState: EntityState = { @@ -9,8 +9,12 @@ export function createStateOperator( entities: { ...state.entities }, }; - mutator(arg, clonedEntityState); + const didMutate = mutator(arg, clonedEntityState); - return Object.assign({}, state, clonedEntityState); + if (didMutate) { + return Object.assign({}, state, clonedEntityState); + } + + return state; }; } diff --git a/modules/entity/src/unsorted_state_adapter.ts b/modules/entity/src/unsorted_state_adapter.ts index afc6980055..3f1cad07e7 100644 --- a/modules/entity/src/unsorted_state_adapter.ts +++ b/modules/entity/src/unsorted_state_adapter.ts @@ -6,46 +6,59 @@ export function createUnsortedStateAdapter( ): EntityStateAdapter { type R = EntityState; - function addOneMutably(entity: T, state: R): void { + function addOneMutably(entity: T, state: R): boolean { const key = selectId(entity); - const index = state.ids.indexOf(key); - if (index !== -1) { - return; + if (key in state.entities) { + return false; } state.ids.push(key); state.entities[key] = entity; + + return true; } - function addManyMutably(entities: T[], state: R): void { + function addManyMutably(entities: T[], state: R): boolean { + let didMutate = false; + for (let index in entities) { - addOneMutably(entities[index], state); + didMutate = addOneMutably(entities[index], state) || didMutate; } + + return didMutate; } - function addAllMutably(entities: T[], state: R): void { + function addAllMutably(entities: T[], state: R): boolean { state.ids = []; state.entities = {}; addManyMutably(entities, state); + + return true; } - function removeOneMutably(key: string, state: R): void { + function removeOneMutably(key: string, state: R): boolean { const index = state.ids.indexOf(key); if (index === -1) { - return; + return false; } state.ids.splice(index, 1); delete state.entities[key]; + + return true; } - function removeManyMutably(keys: string[], state: R): void { + function removeManyMutably(keys: string[], state: R): boolean { + let didMutate = false; + for (let index in keys) { - removeOneMutably(keys[index], state); + didMutate = removeOneMutably(keys[index], state) || didMutate; } + + return didMutate; } function removeAll(state: S): S { @@ -55,11 +68,11 @@ export function createUnsortedStateAdapter( }); } - function updateOneMutably(update: Update, state: R): void { + function updateOneMutably(update: Update, state: R): boolean { const index = state.ids.indexOf(update.id); if (index === -1) { - return; + return false; } const original = state.entities[update.id]; @@ -72,12 +85,18 @@ export function createUnsortedStateAdapter( } state.entities[newKey] = updated; + + return true; } - function updateManyMutably(updates: Update[], state: R): void { + function updateManyMutably(updates: Update[], state: R): boolean { + let didMutate = false; + for (let index in updates) { - updateOneMutably(updates[index], state); + didMutate = updateOneMutably(updates[index], state) || didMutate; } + + return didMutate; } return { From bfdd46208f98d3bc194995a9796c915b18eb9e3f Mon Sep 17 00:00:00 2001 From: BrainCrumbz Date: Fri, 18 Aug 2017 18:42:43 +0200 Subject: [PATCH 37/67] docs(Store): Fix type for metareducers array (#296) --- docs/store/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/store/api.md b/docs/store/api.md index b908e12713..9143cedbfe 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -62,7 +62,7 @@ export function debug(reducer: ActionReducer): ActionReducer { } } -export const metaReducers: MetaReducer = [debug]; +export const metaReducers: MetaReducer[] = [debug]; @NgModule({ imports: [ From d3a3ae7409e9ae2965f56ff87388e39e3edec8cd Mon Sep 17 00:00:00 2001 From: Sharikov Vladislav Date: Fri, 18 Aug 2017 19:43:38 +0300 Subject: [PATCH 38/67] chore(docs): Fix missing back/forward imports for router-store migration (#294) --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 84e72965ee..692fec5c85 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -424,7 +424,7 @@ custom navigation actions that use the `Router` within effects to navigate. BEFORE: ```ts -import { go } from '@ngrx/router-store'; +import { go, back, forward } from '@ngrx/router-store'; store.dispatch(go(['/path', { routeParam: 1 }], { page: 1 }, { replaceUrl: false })); From 54747cfcae7f612c086bc81ba3ab6333e14be54e Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Fri, 18 Aug 2017 11:45:15 -0500 Subject: [PATCH 39/67] fix(Effects): Do not complete effects if one source errors or completes (#297) Closes #232 --- modules/effects/spec/effect_sources.spec.ts | 20 +++++++++++++++++++- modules/effects/src/effect_sources.ts | 17 +++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/modules/effects/spec/effect_sources.spec.ts b/modules/effects/spec/effect_sources.spec.ts index 1f9627f445..1661a6e91c 100644 --- a/modules/effects/spec/effect_sources.spec.ts +++ b/modules/effects/spec/effect_sources.spec.ts @@ -1,8 +1,10 @@ import 'rxjs/add/operator/concat'; import 'rxjs/add/operator/catch'; -import { cold } from 'jasmine-marbles'; +import 'rxjs/add/operator/map'; +import { cold, getTestScheduler } from 'jasmine-marbles'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; +import { timer } from 'rxjs/observable/timer'; import { _throw } from 'rxjs/observable/throw'; import { never } from 'rxjs/observable/never'; import { empty } from 'rxjs/observable/empty'; @@ -69,6 +71,11 @@ describe('EffectSources', () => { @Effect() e$ = _throw(error); } + class SourceG { + @Effect() empty = of('value'); + @Effect() never = timer(50, getTestScheduler()).map(() => 'update'); + } + it('should resolve effects from instances', () => { const sources$ = cold('--a--', { a: new SourceA() }); const expected = cold('--a--', { a }); @@ -99,6 +106,17 @@ describe('EffectSources', () => { expect(mockErrorReporter.report).toHaveBeenCalled(); }); + it('should not complete the group if just one effect completes', () => { + const sources$ = cold('g', { + g: new SourceG(), + }); + const expected = cold('a----b-----', { a: 'value', b: 'update' }); + + const output = toActions(sources$); + + expect(output).toBeObservable(expected); + }); + function toActions(source: any): Observable { source['errorReporter'] = mockErrorReporter; return effectSources.toActions.call(source); diff --git a/modules/effects/src/effect_sources.ts b/modules/effects/src/effect_sources.ts index 3991bbb22e..0ec6037db8 100644 --- a/modules/effects/src/effect_sources.ts +++ b/modules/effects/src/effect_sources.ts @@ -3,9 +3,11 @@ import { mergeMap } from 'rxjs/operator/mergeMap'; import { exhaustMap } from 'rxjs/operator/exhaustMap'; import { map } from 'rxjs/operator/map'; import { dematerialize } from 'rxjs/operator/dematerialize'; +import { filter } from 'rxjs/operator/filter'; import { concat } from 'rxjs/observable/concat'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; +import { Notification } from 'rxjs/Notification'; import { Injectable } from '@angular/core'; import { Action } from '@ngrx/store'; import { EffectNotification, verifyOutput } from './effect_notification'; @@ -31,13 +33,16 @@ export class EffectSources extends Subject { groupBy.call(this, getSourceForInstance), (source$: GroupedObservable) => dematerialize.call( - map.call( - exhaustMap.call(source$, resolveEffectSource), - (output: EffectNotification) => { - verifyOutput(output, this.errorReporter); + filter.call( + map.call( + exhaustMap.call(source$, resolveEffectSource), + (output: EffectNotification) => { + verifyOutput(output, this.errorReporter); - return output.notification; - } + return output.notification; + } + ), + (notification: Notification) => notification.kind === 'N' ) ) ); From ffa969417d6a393c86615bd7e765e1df7fdf82d8 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Fri, 18 Aug 2017 11:45:38 -0500 Subject: [PATCH 40/67] v4.0.5 --- lerna.json | 2 +- modules/effects/CHANGELOG.md | 121 ++++++++++++++++++++++++++++++++++- modules/effects/package.json | 2 +- 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/lerna.json b/lerna.json index 7e11594192..a397f85204 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "modules/*" ], - "version": "4.0.4", + "version": "4.0.5", "npmClient": "yarn" } diff --git a/modules/effects/CHANGELOG.md b/modules/effects/CHANGELOG.md index 38ffce669a..27feb8f35c 100644 --- a/modules/effects/CHANGELOG.md +++ b/modules/effects/CHANGELOG.md @@ -3,7 +3,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - + +## 4.0.5 (2017-08-18) + + +### Bug Fixes + +* **Effects:** Do not complete effects if one source errors or completes (#297) ([54747cf](https://github.com/ngrx/platform/commit/54747cf)), closes [#232](https://github.com/ngrx/platform/issues/232) + + + + ## 4.0.4 (2017-08-17) @@ -108,7 +118,17 @@ export class SomeFeatureModule { } - + +## 4.0.4 (2017-08-17) + + +### Bug Fixes + +* **Effects:** Use factory provide for console (#288) ([bf7f70c](https://github.com/ngrx/platform/commit/bf7f70c)), closes [#276](https://github.com/ngrx/platform/issues/276) + + + + ## 4.0.3 (2017-08-16) @@ -203,7 +223,102 @@ export class SomeFeatureModule { } - + +## 4.0.3 (2017-08-16) + + +### Bug Fixes + +* **Effects:** Deprecate toPayload utility function (#266) ([1cbb2c9](https://github.com/ngrx/platform/commit/1cbb2c9)) +* **Effects:** Ensure StoreModule is loaded before effects (#230) ([065d33e](https://github.com/ngrx/platform/commit/065d33e)), closes [#184](https://github.com/ngrx/platform/issues/184) [#219](https://github.com/ngrx/platform/issues/219) +* **Effects:** Export EffectsNotification interface (#231) ([2b1a076](https://github.com/ngrx/platform/commit/2b1a076)) + + + + +## 4.0.2 (2017-08-02) + + +### Bug Fixes + +* **Effects:** Wrap testing source in an Actions observable (#121) ([bfdb83b](https://github.com/ngrx/platform/commit/bfdb83b)), closes [#117](https://github.com/ngrx/platform/issues/117) + + +### Features + +* **Effects:** Add generic type to the "ofType" operator ([55c13b2](https://github.com/ngrx/platform/commit/55c13b2)) +* **Store:** Add injection token option for feature modules (#153) ([7f77693](https://github.com/ngrx/platform/commit/7f77693)), closes [#116](https://github.com/ngrx/platform/issues/116) [#141](https://github.com/ngrx/platform/issues/141) [#147](https://github.com/ngrx/platform/issues/147) + + + + +## 4.0.1 (2017-07-18) + + +### Bug Fixes + +* **build:** Get tests running for each project ([c4a1054](https://github.com/ngrx/platform/commit/c4a1054)) +* **effects:** allow downleveled annotations (#98) ([875b326](https://github.com/ngrx/platform/commit/875b326)), closes [#93](https://github.com/ngrx/platform/issues/93) +* **Effects:** Start child effects after running root effects (#43) ([931adb1](https://github.com/ngrx/platform/commit/931adb1)) +* **Effects:** Use Actions generic type for the return of the ofType operator ([d176a11](https://github.com/ngrx/platform/commit/d176a11)) + + +### Code Refactoring + +* **Effects:** Simplified AP, added better error reporting and effects stream control ([015107f](https://github.com/ngrx/platform/commit/015107f)) + + +### Features + +* **build:** Updated build pipeline for modules ([68bd9df](https://github.com/ngrx/platform/commit/68bd9df)) +* **Effects:** Ensure effects are only subscribed to once ([089abdc](https://github.com/ngrx/platform/commit/089abdc)) +* **Effects:** Introduce new Effects testing module (#70) ([7dbb571](https://github.com/ngrx/platform/commit/7dbb571)) + + +### BREAKING CHANGES + +* **Effects:** Effects API for registering effects has been updated to allow for multiple classes to be provided. + +BEFORE: +```ts +@NgModule({ +imports: [ +EffectsModule.run(SourceA), +EffectsModule.run(SourceB) +] +}) +export class AppModule { } +``` + +AFTER: +```ts +@NgModule({ +imports: [ +EffectsModule.forRoot([ +SourceA, +SourceB, +SourceC, +]) +] +}) +export class AppModule { } + +@NgModule({ +imports: [ +EffectsModule.forFeature([ +FeatureSourceA, +FeatureSourceB, +FeatureSourceC, +]) +] +}) +export class SomeFeatureModule { } +``` + + + + + # 4.0.0 (2017-07-18) diff --git a/modules/effects/package.json b/modules/effects/package.json index 2777686d33..23db08a6e4 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -1,6 +1,6 @@ { "name": "@ngrx/effects", - "version": "4.0.4", + "version": "4.0.5", "description": "Side effect model for @ngrx/store", "module": "@ngrx/effects.es5.js", "es2015": "@ngrx/effects.js", From d20ebf112b629b11c7b4d4b47c8dbf8b96449521 Mon Sep 17 00:00:00 2001 From: Sharikov Vladislav Date: Fri, 18 Aug 2017 23:31:42 +0300 Subject: [PATCH 41/67] chore(docs): Fix contributors link in readme (#299) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c14414609..580d6a81ba 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Reactive libraries for Angular ## Contributing Please read [contributing guidelines here](./CONTRIBUTING.md). - + ## Backers From 56cb21fd3f1ac69eac252c47372bdbd81291494e Mon Sep 17 00:00:00 2001 From: 03byron Date: Sun, 20 Aug 2017 19:42:06 +0200 Subject: [PATCH 42/67] feat(createSelector): Expose projector function on selectors to improve testability Closes #290 --- modules/store/spec/selector.spec.ts | 11 +++++++++++ modules/store/src/selector.ts | 12 ++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index ac563fdabe..68aefe0635 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -41,6 +41,17 @@ describe('Selectors', () => { expect(projectFn).toHaveBeenCalledWith(countOne, countTwo); }); + it('should be possible to test a projector fn independent from the selectors it is composed of', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector(incrementOne, incrementTwo, projectFn); + + selector.projector('', ''); + + expect(incrementOne).not.toHaveBeenCalled(); + expect(incrementTwo).not.toHaveBeenCalled(); + expect(projectFn).toHaveBeenCalledWith('', ''); + }); + it('should call the projector function only when the value of a dependent selector change', () => { const firstState = { first: 'state', unchanged: 'state' }; const secondState = { second: 'state', unchanged: 'state' }; diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index 2a20df84ad..8f42733dd3 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -1,12 +1,13 @@ import { Selector } from './models'; +export type AnyFn = (...args: any[]) => any; + export interface MemoizedSelector extends Selector { release(): void; + projector: AnyFn; } -export type AnyFn = (...args: any[]) => any; - export function memoize(t: AnyFn): { memoized: AnyFn; reset: () => void } { let lastArguments: null | IArguments = null; let lastResult: any = null; @@ -132,7 +133,10 @@ export function createSelector(...args: any[]): Selector { memoizedSelectors.forEach(selector => selector.release()); } - return Object.assign(memoizedState.memoized, { release }); + return Object.assign(memoizedState.memoized, { + release, + projector: memoizedProjector.memoized, + }); } export function createFeatureSelector( @@ -142,5 +146,5 @@ export function createFeatureSelector( return state[featureName]; }); - return Object.assign(memoized, { release: reset }); + return Object.assign(memoized, { release: reset, projector: memoized }); } From bf14d67334c0382d68a0988b5a0f4424aa459c21 Mon Sep 17 00:00:00 2001 From: Ryan Jordan Date: Wed, 23 Aug 2017 21:13:04 -0500 Subject: [PATCH 43/67] chore(precommit): Fix precommit hook to git add after changing files (#319) --- package.json | 13 +++- yarn.lock | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 215 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 984a0f6dbc..d2262dcd81 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "4.0.0", "description": "monorepo for ngrx development", "scripts": { - "precommit": "yarn run prettier", + "precommit": "lint-staged", "bootstrap": "lerna bootstrap", "build": "ts-node ./build/index.ts", "deploy:builds": "ts-node ./build/deploy-build.ts", @@ -16,12 +16,18 @@ "example:start:aot": "yarn run build && yarn run cli -- serve --aot", "example:test": "yarn run cli -- test --code-coverage", "ci": "yarn run build && yarn run test && nyc report --reporter=text-lcov | coveralls", - "prettier": "prettier --parser typescript --single-quote --trailing-comma --write \"./**/*.ts\"", + "prettier": "prettier --parser typescript --single-quote --trailing-comma es5 --write \"./**/*.ts\"", "watch:tests": "chokidar 'modules/**/*.ts' --initial -c 'nyc --reporter=text --reporter=html yarn run test:unit'", "postinstall": "opencollective postinstall", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", "release": "lerna publish --skip-npm --conventional-commits && npm run build" }, + "lint-staged": { + "*.ts": [ + "yarn prettier", + "git add" + ] + }, "keywords": [ "ngrx", "angular", @@ -86,6 +92,7 @@ "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "lerna": "^2.0.0", + "lint-staged": "^4.0.3", "module-alias": "^2.0.0", "ngrx-store-freeze": "^0.1.9", "nyc": "^10.1.2", @@ -113,4 +120,4 @@ "url": "https://opencollective.com/ngrx", "logo": "https://opencollective.com/opencollective/logo.txt" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 123cdf2402..64eefd0870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -317,7 +317,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.1.0: +ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -346,7 +346,7 @@ anymatch@^1.1.0, anymatch@^1.3.0: arrify "^1.0.0" micromatch "^2.1.5" -app-root-path@^2.0.1: +app-root-path@^2.0.0, app-root-path@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" @@ -980,16 +980,33 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" +cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" dependencies: restore-cursor "^2.0.0" +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + cli-spinners@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.0.0.tgz#ef987ed3d48391ac3dab9180b406a742180d6e6a" +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" @@ -1405,6 +1422,19 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37" + dependencies: + graceful-fs "^4.1.2" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.0.1" + os-homedir "^1.0.1" + parse-json "^2.2.0" + pinkie-promise "^2.0.0" + require-from-string "^1.1.0" + cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.3.tgz#952771eb0dddc1cb3fa2f6fbe51a522e93b3ee0a" @@ -1661,6 +1691,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^1.27.2: + version "1.28.5" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1906,6 +1940,10 @@ electron-to-chromium@^1.2.7: version "1.3.15" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369" +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2098,6 +2136,22 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -2218,6 +2272,13 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" +figures@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -2916,6 +2977,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -3192,6 +3257,10 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -3638,6 +3707,67 @@ license-webpack-plugin@^0.4.2: dependencies: object-assign "^4.1.0" +lint-staged@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-4.0.3.tgz#1ce55591bc2c83a781a90b69a0a0c8aa0fc6370b" + dependencies: + app-root-path "^2.0.0" + cosmiconfig "^1.1.0" + execa "^0.8.0" + listr "^0.12.0" + lodash.chunk "^4.2.0" + minimatch "^3.0.0" + npm-which "^3.0.1" + p-map "^1.1.1" + staged-git-files "0.0.4" + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + +listr-update-renderer@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f" + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +listr@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + figures "^1.7.0" + indent-string "^2.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.2.0" + listr-verbose-renderer "^0.4.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + ora "^0.2.3" + p-map "^1.1.1" + rxjs "^5.0.0-beta.11" + stream-to-observable "^0.1.0" + strip-ansi "^3.0.1" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -3697,6 +3827,10 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -3752,6 +3886,13 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-update@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + dependencies: + ansi-escapes "^1.0.0" + cli-cursor "^1.0.2" + log4js@^0.6.31: version "0.6.38" resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd" @@ -4168,12 +4309,26 @@ normalize-url@^1.4.0: query-string "^4.1.0" sort-keys "^1.0.0" +npm-path@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe" + dependencies: + which "^1.2.10" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" dependencies: path-key "^2.0.0" +npm-which@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa" + dependencies: + commander "^2.9.0" + npm-path "^2.0.2" + which "^1.2.10" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -4276,6 +4431,10 @@ once@^1.3.0, once@^1.3.3, once@^1.4.0: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + onetime@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.0.tgz#52aa8110e52fc5126ffc667bd8ec21c2ed209ce6" @@ -4317,6 +4476,15 @@ options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" +ora@^0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + ora@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ora/-/ora-1.3.0.tgz#80078dd2b92a934af66a3ad72a5b910694ede51a" @@ -4379,6 +4547,10 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -5261,6 +5433,13 @@ resolve@^1.1.6, resolve@^1.1.7: dependencies: path-parse "^1.0.5" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -5315,6 +5494,12 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" +rxjs@^5.0.0-beta.11: + version "5.4.3" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" + dependencies: + symbol-observable "^1.0.1" + rxjs@^5.0.1, rxjs@^5.4.0: version "5.4.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7" @@ -5540,6 +5725,10 @@ silent-error@^1.0.0: dependencies: debug "^2.2.0" +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -5760,6 +5949,10 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +staged-git-files@0.0.4: + version "0.0.4" + resolved "https://registry.npmjs.org/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" + "statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -5787,6 +5980,10 @@ stream-http@^2.3.1: to-arraybuffer "^1.0.0" xtend "^4.0.0" +stream-to-observable@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -6526,6 +6723,12 @@ which@1, which@^1.2.1, which@^1.2.4, which@^1.2.9: dependencies: isexe "^1.1.1" +which@^1.2.10: + version "1.3.0" + resolved "https://registry.npmjs.org/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" From ae7d5e11d0b57afdf67fcd9c28118d2c90efad40 Mon Sep 17 00:00:00 2001 From: ukrukarg Date: Thu, 24 Aug 2017 11:21:36 -0400 Subject: [PATCH 44/67] fix(combineSelectors): Remove default parameter from function signature for Closure --- modules/store/src/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/store/src/utils.ts b/modules/store/src/utils.ts index fb1d48a24c..aa83cc1080 100644 --- a/modules/store/src/utils.ts +++ b/modules/store/src/utils.ts @@ -26,7 +26,8 @@ export function combineReducers( const finalReducerKeys = Object.keys(finalReducers); - return function combination(state = initialState, action) { + return function combination(state, action) { + state = state || initialState; let hasChanged = false; const nextState: any = {}; for (let i = 0; i < finalReducerKeys.length; i++) { From e4133a458590c96590a07be46914e9e795282992 Mon Sep 17 00:00:00 2001 From: Ryan Jordan Date: Fri, 25 Aug 2017 07:15:14 -0500 Subject: [PATCH 45/67] docs(api): Fix imports typo to show imports from correct package (#322) --- docs/router-store/api.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/router-store/api.md b/docs/router-store/api.md index 052b9b58f3..4282f233ae 100644 --- a/docs/router-store/api.md +++ b/docs/router-store/api.md @@ -9,13 +9,12 @@ To use the time-traveling debugging in the Devtools, you must return an object c ```ts import { StoreModule, ActionReducerMap } from '@ngrx/store'; -import { Params } from '@angular/router'; +import { Params, RouterStateSnapshot } from '@angular/router'; import { StoreRouterConnectingModule, routerReducer, RouterReducerState, - RouterStateSerializer, - RouterStateSnapshot + RouterStateSerializer } from '@ngrx/router-store'; export interface RouterStateUrl { From ebf9cf496a1c72e48e54f54694a8df205864bd04 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 25 Aug 2017 09:30:53 -0500 Subject: [PATCH 46/67] docs(Migration): Add payload interface options to migration docs (#326) Closes #303 --- MIGRATION.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 692fec5c85..0799c6eb5b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -43,7 +43,9 @@ import { compose } from '@ngrx/store'; ### Action interface -The `payload` property has been removed from the `Action` interface. +The `payload` property has been removed from the `Action` interface. It was a source of type-safety +issues, especially when used with `@ngrx/effects`. If your interface/class has a payload, you need to provide +the type. BEFORE: ```ts @@ -79,6 +81,22 @@ export class MyEffects { } ``` +If you prefer to keep the `payload` interface property, you can provide your own parameterized version. + +```ts +export interface ActionWithPayload extends Action { + payload: T; +} +``` + +And if you need an unsafe version to help with transition. + +```ts +export interface UnsafeAction implements Action { + payload?: any; +} +``` + ### Registering Reducers Previously to be AOT compatible, it was required to pass a function to the `provideStore` method to compose the reducers into one root reducer. The `initialState` was also provided to the method as an object in the second argument. From b8ebe2e106fec774fe325735ff3f66ba9f99661a Mon Sep 17 00:00:00 2001 From: tdeschryver Date: Mon, 28 Aug 2017 18:24:28 +0200 Subject: [PATCH 47/67] chore(Example): rename Actions to be consistent (#330) --- example-app/app/books/actions/book.ts | 14 +++----- example-app/app/books/actions/collection.ts | 36 +++++++++---------- .../app/books/containers/collection-page.ts | 2 +- .../app/books/containers/find-book-page.ts | 2 +- .../books/containers/selected-book-page.ts | 4 +-- .../app/books/containers/view-book-page.ts | 2 +- example-app/app/books/effects/book.spec.ts | 16 ++++----- example-app/app/books/effects/book.ts | 6 ++-- .../app/books/effects/collection.spec.ts | 36 +++++++++---------- example-app/app/books/effects/collection.ts | 16 ++++----- example-app/app/books/guards/book-exists.ts | 4 +-- example-app/app/books/reducers/book.spec.ts | 22 +++++------- example-app/app/core/actions/layout.ts | 6 ++-- example-app/app/core/containers/app.ts | 6 ++-- 14 files changed, 82 insertions(+), 90 deletions(-) diff --git a/example-app/app/books/actions/book.ts b/example-app/app/books/actions/book.ts index e13684906f..e1735e87fb 100644 --- a/example-app/app/books/actions/book.ts +++ b/example-app/app/books/actions/book.ts @@ -13,25 +13,25 @@ export const SELECT = '[Book] Select'; * * See Discriminated Unions: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions */ -export class SearchAction implements Action { +export class Search implements Action { readonly type = SEARCH; constructor(public payload: string) {} } -export class SearchCompleteAction implements Action { +export class SearchComplete implements Action { readonly type = SEARCH_COMPLETE; constructor(public payload: Book[]) {} } -export class LoadAction implements Action { +export class Load implements Action { readonly type = LOAD; constructor(public payload: Book) {} } -export class SelectAction implements Action { +export class Select implements Action { readonly type = SELECT; constructor(public payload: string) {} @@ -41,8 +41,4 @@ export class SelectAction implements Action { * Export a type alias of all actions in this action group * so that reducers can easily compose action types */ -export type Actions = - | SearchAction - | SearchCompleteAction - | LoadAction - | SelectAction; +export type Actions = Search | SearchComplete | Load | Select; diff --git a/example-app/app/books/actions/collection.ts b/example-app/app/books/actions/collection.ts index 4cd1c898ec..7de653bc16 100644 --- a/example-app/app/books/actions/collection.ts +++ b/example-app/app/books/actions/collection.ts @@ -14,19 +14,19 @@ export const LOAD_FAIL = '[Collection] Load Fail'; /** * Add Book to Collection Actions */ -export class AddBookAction implements Action { +export class AddBook implements Action { readonly type = ADD_BOOK; constructor(public payload: Book) {} } -export class AddBookSuccessAction implements Action { +export class AddBookSuccess implements Action { readonly type = ADD_BOOK_SUCCESS; constructor(public payload: Book) {} } -export class AddBookFailAction implements Action { +export class AddBookFail implements Action { readonly type = ADD_BOOK_FAIL; constructor(public payload: Book) {} @@ -35,19 +35,19 @@ export class AddBookFailAction implements Action { /** * Remove Book from Collection Actions */ -export class RemoveBookAction implements Action { +export class RemoveBook implements Action { readonly type = REMOVE_BOOK; constructor(public payload: Book) {} } -export class RemoveBookSuccessAction implements Action { +export class RemoveBookSuccess implements Action { readonly type = REMOVE_BOOK_SUCCESS; constructor(public payload: Book) {} } -export class RemoveBookFailAction implements Action { +export class RemoveBookFail implements Action { readonly type = REMOVE_BOOK_FAIL; constructor(public payload: Book) {} @@ -56,29 +56,29 @@ export class RemoveBookFailAction implements Action { /** * Load Collection Actions */ -export class LoadAction implements Action { +export class Load implements Action { readonly type = LOAD; } -export class LoadSuccessAction implements Action { +export class LoadSuccess implements Action { readonly type = LOAD_SUCCESS; constructor(public payload: Book[]) {} } -export class LoadFailAction implements Action { +export class LoadFail implements Action { readonly type = LOAD_FAIL; constructor(public payload: any) {} } export type Actions = - | AddBookAction - | AddBookSuccessAction - | AddBookFailAction - | RemoveBookAction - | RemoveBookSuccessAction - | RemoveBookFailAction - | LoadAction - | LoadSuccessAction - | LoadFailAction; + | AddBook + | AddBookSuccess + | AddBookFail + | RemoveBook + | RemoveBookSuccess + | RemoveBookFail + | Load + | LoadSuccess + | LoadFail; diff --git a/example-app/app/books/containers/collection-page.ts b/example-app/app/books/containers/collection-page.ts index 89876c77f7..f4c61e9e32 100644 --- a/example-app/app/books/containers/collection-page.ts +++ b/example-app/app/books/containers/collection-page.ts @@ -40,6 +40,6 @@ export class CollectionPageComponent implements OnInit { } ngOnInit() { - this.store.dispatch(new collection.LoadAction()); + this.store.dispatch(new collection.Load()); } } diff --git a/example-app/app/books/containers/find-book-page.ts b/example-app/app/books/containers/find-book-page.ts index d24d01be03..86fda79483 100644 --- a/example-app/app/books/containers/find-book-page.ts +++ b/example-app/app/books/containers/find-book-page.ts @@ -27,6 +27,6 @@ export class FindBookPageComponent { } search(query: string) { - this.store.dispatch(new book.SearchAction(query)); + this.store.dispatch(new book.Search(query)); } } diff --git a/example-app/app/books/containers/selected-book-page.ts b/example-app/app/books/containers/selected-book-page.ts index d271ba1989..5fd8579ca9 100644 --- a/example-app/app/books/containers/selected-book-page.ts +++ b/example-app/app/books/containers/selected-book-page.ts @@ -30,10 +30,10 @@ export class SelectedBookPageComponent { } addToCollection(book: Book) { - this.store.dispatch(new collection.AddBookAction(book)); + this.store.dispatch(new collection.AddBook(book)); } removeFromCollection(book: Book) { - this.store.dispatch(new collection.RemoveBookAction(book)); + this.store.dispatch(new collection.RemoveBook(book)); } } diff --git a/example-app/app/books/containers/view-book-page.ts b/example-app/app/books/containers/view-book-page.ts index 6acd0a73b3..f879d84ee4 100644 --- a/example-app/app/books/containers/view-book-page.ts +++ b/example-app/app/books/containers/view-book-page.ts @@ -30,7 +30,7 @@ export class ViewBookPageComponent implements OnDestroy { constructor(store: Store, route: ActivatedRoute) { this.actionsSubscription = route.params - .map(params => new book.SelectAction(params.id)) + .map(params => new book.Select(params.id)) .subscribe(store); } diff --git a/example-app/app/books/effects/book.spec.ts b/example-app/app/books/effects/book.spec.ts index e5b2ce3c12..dbe58a4bf5 100644 --- a/example-app/app/books/effects/book.spec.ts +++ b/example-app/app/books/effects/book.spec.ts @@ -5,7 +5,7 @@ import { empty } from 'rxjs/observable/empty'; import { BookEffects, SEARCH_SCHEDULER, SEARCH_DEBOUNCE } from './book'; import { GoogleBooksService } from '../../core/services/google-books'; import { Observable } from 'rxjs/Observable'; -import { SearchAction, SearchCompleteAction } from '../actions/book'; +import { Search, SearchComplete } from '../actions/book'; import { Book } from '../models/book'; export class TestActions extends Actions { @@ -47,12 +47,12 @@ describe('BookEffects', () => { }); describe('search$', () => { - it('should return a new book.SearchCompleteAction, with the books, on success, after the de-bounce', () => { + it('should return a new book.SearchComplete, with the books, on success, after the de-bounce', () => { const book1 = { id: '111', volumeInfo: {} } as Book; const book2 = { id: '222', volumeInfo: {} } as Book; const books = [book1, book2]; - const action = new SearchAction('query'); - const completion = new SearchCompleteAction(books); + const action = new Search('query'); + const completion = new SearchComplete(books); actions$.stream = hot('-a---', { a: action }); const response = cold('-a|', { a: books }); @@ -62,9 +62,9 @@ describe('BookEffects', () => { expect(effects.search$).toBeObservable(expected); }); - it('should return a new book.SearchCompleteAction, with an empty array, if the books service throws', () => { - const action = new SearchAction('query'); - const completion = new SearchCompleteAction([]); + it('should return a new book.SearchComplete, with an empty array, if the books service throws', () => { + const action = new Search('query'); + const completion = new SearchComplete([]); const error = 'Error!'; actions$.stream = hot('-a---', { a: action }); @@ -76,7 +76,7 @@ describe('BookEffects', () => { }); it(`should not do anything if the query is an empty string`, () => { - const action = new SearchAction(''); + const action = new Search(''); actions$.stream = hot('-a---', { a: action }); const expected = cold('---'); diff --git a/example-app/app/books/effects/book.ts b/example-app/app/books/effects/book.ts index 76cee2e628..75804c8347 100644 --- a/example-app/app/books/effects/book.ts +++ b/example-app/app/books/effects/book.ts @@ -37,7 +37,7 @@ export const SEARCH_SCHEDULER = new InjectionToken( export class BookEffects { @Effect() search$: Observable = this.actions$ - .ofType(book.SEARCH) + .ofType(book.SEARCH) .debounceTime(this.debounce, this.scheduler || async) .map(action => action.payload) .switchMap(query => { @@ -50,8 +50,8 @@ export class BookEffects { return this.googleBooks .searchBooks(query) .takeUntil(nextSearch$) - .map((books: Book[]) => new book.SearchCompleteAction(books)) - .catch(() => of(new book.SearchCompleteAction([]))); + .map((books: Book[]) => new book.SearchComplete(books)) + .catch(() => of(new book.SearchComplete([]))); }); constructor( diff --git a/example-app/app/books/effects/collection.spec.ts b/example-app/app/books/effects/collection.spec.ts index 3ff6954e3d..c2a4eb8774 100644 --- a/example-app/app/books/effects/collection.spec.ts +++ b/example-app/app/books/effects/collection.spec.ts @@ -60,9 +60,9 @@ describe('CollectionEffects', () => { }); describe('loadCollection$', () => { - it('should return a collection.LoadSuccessAction, with the books, on success', () => { - const action = new collection.LoadAction(); - const completion = new collection.LoadSuccessAction([book1, book2]); + it('should return a collection.LoadSuccess, with the books, on success', () => { + const action = new collection.Load(); + const completion = new collection.LoadSuccess([book1, book2]); actions$.stream = hot('-a', { a: action }); const response = cold('-a-b|', { a: book1, b: book2 }); @@ -72,10 +72,10 @@ describe('CollectionEffects', () => { expect(effects.loadCollection$).toBeObservable(expected); }); - it('should return a collection.LoadFailAction, if the query throws', () => { - const action = new collection.LoadAction(); + it('should return a collection.LoadFail, if the query throws', () => { + const action = new collection.Load(); const error = 'Error!'; - const completion = new collection.LoadFailAction(error); + const completion = new collection.LoadFail(error); actions$.stream = hot('-a', { a: action }); const response = cold('-#', {}, error); @@ -87,9 +87,9 @@ describe('CollectionEffects', () => { }); describe('addBookToCollection$', () => { - it('should return a collection.AddBookSuccessAction, with the book, on success', () => { - const action = new collection.AddBookAction(book1); - const completion = new collection.AddBookSuccessAction(book1); + it('should return a collection.AddBookSuccess, with the book, on success', () => { + const action = new collection.AddBook(book1); + const completion = new collection.AddBookSuccess(book1); actions$.stream = hot('-a', { a: action }); const response = cold('-b', { b: true }); @@ -100,9 +100,9 @@ describe('CollectionEffects', () => { expect(db.insert).toHaveBeenCalledWith('books', [book1]); }); - it('should return a collection.AddBookFailAction, with the book, when the db insert throws', () => { - const action = new collection.AddBookAction(book1); - const completion = new collection.AddBookFailAction(book1); + it('should return a collection.AddBookFail, with the book, when the db insert throws', () => { + const action = new collection.AddBook(book1); + const completion = new collection.AddBookFail(book1); const error = 'Error!'; actions$.stream = hot('-a', { a: action }); @@ -114,9 +114,9 @@ describe('CollectionEffects', () => { }); describe('removeBookFromCollection$', () => { - it('should return a collection.RemoveBookSuccessAction, with the book, on success', () => { - const action = new collection.RemoveBookAction(book1); - const completion = new collection.RemoveBookSuccessAction(book1); + it('should return a collection.RemoveBookSuccess, with the book, on success', () => { + const action = new collection.RemoveBook(book1); + const completion = new collection.RemoveBookSuccess(book1); actions$.stream = hot('-a', { a: action }); const response = cold('-b', { b: true }); @@ -129,9 +129,9 @@ describe('CollectionEffects', () => { ]); }); - it('should return a collection.RemoveBookFailAction, with the book, when the db insert throws', () => { - const action = new collection.RemoveBookAction(book1); - const completion = new collection.RemoveBookFailAction(book1); + it('should return a collection.RemoveBookFail, with the book, when the db insert throws', () => { + const action = new collection.RemoveBook(book1); + const completion = new collection.RemoveBookFail(book1); const error = 'Error!'; actions$.stream = hot('-a', { a: action }); diff --git a/example-app/app/books/effects/collection.ts b/example-app/app/books/effects/collection.ts index d14ecebba3..a86c8c11c3 100644 --- a/example-app/app/books/effects/collection.ts +++ b/example-app/app/books/effects/collection.ts @@ -38,30 +38,30 @@ export class CollectionEffects { this.db .query('books') .toArray() - .map((books: Book[]) => new collection.LoadSuccessAction(books)) - .catch(error => of(new collection.LoadFailAction(error))) + .map((books: Book[]) => new collection.LoadSuccess(books)) + .catch(error => of(new collection.LoadFail(error))) ); @Effect() addBookToCollection$: Observable = this.actions$ .ofType(collection.ADD_BOOK) - .map((action: collection.AddBookAction) => action.payload) + .map((action: collection.AddBook) => action.payload) .mergeMap(book => this.db .insert('books', [book]) - .map(() => new collection.AddBookSuccessAction(book)) - .catch(() => of(new collection.AddBookFailAction(book))) + .map(() => new collection.AddBookSuccess(book)) + .catch(() => of(new collection.AddBookFail(book))) ); @Effect() removeBookFromCollection$: Observable = this.actions$ .ofType(collection.REMOVE_BOOK) - .map((action: collection.RemoveBookAction) => action.payload) + .map((action: collection.RemoveBook) => action.payload) .mergeMap(book => this.db .executeWrite('books', 'delete', [book.id]) - .map(() => new collection.RemoveBookSuccessAction(book)) - .catch(() => of(new collection.RemoveBookFailAction(book))) + .map(() => new collection.RemoveBookSuccess(book)) + .catch(() => of(new collection.RemoveBookFail(book))) ); constructor(private actions$: Actions, private db: Database) {} diff --git a/example-app/app/books/guards/book-exists.ts b/example-app/app/books/guards/book-exists.ts index 1d5d5aebc0..469f51a324 100644 --- a/example-app/app/books/guards/book-exists.ts +++ b/example-app/app/books/guards/book-exists.ts @@ -58,8 +58,8 @@ export class BookExistsGuard implements CanActivate { hasBookInApi(id: string): Observable { return this.googleBooks .retrieveBook(id) - .map(bookEntity => new book.LoadAction(bookEntity)) - .do((action: book.LoadAction) => this.store.dispatch(action)) + .map(bookEntity => new book.Load(bookEntity)) + .do((action: book.Load) => this.store.dispatch(action)) .map(book => !!book) .catch(() => { this.router.navigate(['/404']); diff --git a/example-app/app/books/reducers/book.spec.ts b/example-app/app/books/reducers/book.spec.ts index f5e280fc4e..45650f4119 100644 --- a/example-app/app/books/reducers/book.spec.ts +++ b/example-app/app/books/reducers/book.spec.ts @@ -1,12 +1,8 @@ import { reducer } from './books'; import * as fromBooks from './books'; -import { - SearchCompleteAction, - LoadAction, - SelectAction, -} from '../actions/book'; +import { SearchComplete, Load, Select } from '../actions/book'; import { Book } from '../models/book'; -import { LoadSuccessAction } from '../actions/collection'; +import { LoadSuccess } from '../actions/collection'; describe('BooksReducer', () => { describe('undefined action', () => { @@ -68,20 +64,20 @@ describe('BooksReducer', () => { } it('should add all books in the payload when none exist', () => { - noExistingBooks(SearchCompleteAction); - noExistingBooks(LoadSuccessAction); + noExistingBooks(SearchComplete); + noExistingBooks(LoadSuccess); }); it('should add only new books when books already exist', () => { - existingBooks(SearchCompleteAction); - existingBooks(LoadSuccessAction); + existingBooks(SearchComplete); + existingBooks(LoadSuccess); }); }); describe('LOAD', () => { it('should add a single book, if the book does not exist', () => { const book = { id: '888' } as Book; - const action = new LoadAction(book); + const action = new Load(book); const expectedResult = { ids: ['888'], @@ -103,7 +99,7 @@ describe('BooksReducer', () => { }, } as any; const book = { id: '999', foo: 'baz' } as any; - const action = new LoadAction(book); + const action = new Load(book); const result = reducer(initialState, action); expect(result).toEqual(initialState); @@ -112,7 +108,7 @@ describe('BooksReducer', () => { describe('SELECT', () => { it('should set the selected book id on the state', () => { - const action = new SelectAction('1'); + const action = new Select('1'); const result = reducer(fromBooks.initialState, action); expect(result.selectedBookId).toBe('1'); diff --git a/example-app/app/core/actions/layout.ts b/example-app/app/core/actions/layout.ts index 9dcc2aeaee..6331f55d81 100644 --- a/example-app/app/core/actions/layout.ts +++ b/example-app/app/core/actions/layout.ts @@ -3,12 +3,12 @@ import { Action } from '@ngrx/store'; export const OPEN_SIDENAV = '[Layout] Open Sidenav'; export const CLOSE_SIDENAV = '[Layout] Close Sidenav'; -export class OpenSidenavAction implements Action { +export class OpenSidenav implements Action { readonly type = OPEN_SIDENAV; } -export class CloseSidenavAction implements Action { +export class CloseSidenav implements Action { readonly type = CLOSE_SIDENAV; } -export type Actions = OpenSidenavAction | CloseSidenavAction; +export type Actions = OpenSidenav | CloseSidenav; diff --git a/example-app/app/core/containers/app.ts b/example-app/app/core/containers/app.ts index 9643af4d60..ca7722adcf 100644 --- a/example-app/app/core/containers/app.ts +++ b/example-app/app/core/containers/app.ts @@ -22,7 +22,7 @@ import * as Auth from '../../auth/actions/auth'; Sign In - + Sign Out @@ -55,11 +55,11 @@ export class AppComponent { * updates and user interaction through the life of our * application. */ - this.store.dispatch(new layout.CloseSidenavAction()); + this.store.dispatch(new layout.CloseSidenav()); } openSidenav() { - this.store.dispatch(new layout.OpenSidenavAction()); + this.store.dispatch(new layout.OpenSidenav()); } logout() { From 257fc9dd6c343c39f209e705fe82e17773c945c2 Mon Sep 17 00:00:00 2001 From: tdeschryver Date: Mon, 28 Aug 2017 19:03:36 +0200 Subject: [PATCH 48/67] fix(Build): Fix build with space in path (#331) --- build/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/util.ts b/build/util.ts index b26526c4ef..fe73f07106 100644 --- a/build/util.ts +++ b/build/util.ts @@ -176,5 +176,5 @@ export function getBottomLevelName(packageName: string) { } export function baseDir(...dirs: string[]): string { - return path.resolve(__dirname, '../', ...dirs); + return `"${path.resolve(__dirname, '../', ...dirs)}"`; } From b85062fceeb3f577a0aeaad33bc72e8d7ac95e0b Mon Sep 17 00:00:00 2001 From: Ryan Jordan Date: Wed, 30 Aug 2017 11:23:43 -0500 Subject: [PATCH 49/67] docs(example-app): Update example-app url (#336) --- example-app/README.md | 2 +- package.json | 27 +++++++-------------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/example-app/README.md b/example-app/README.md index ed59e3ee39..78c940d8df 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -1,7 +1,7 @@ # @ngrx example application Example application utilizing @ngrx libraries, showcasing common patterns and best practices. -Take a look at the [live app](http://ngrx.github.io/example-app/). +Take a look at the [live app](https://ngrx.github.io/platform/example-app/). This app is a book collection manager. The user can authenticate, use the Google Books API to search for books and add them to their collection. This application utilizes [@ngrx/db](https://github.com/ngrx/db) diff --git a/package.json b/package.json index d2262dcd81..04f8be9bc6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "example:start": "yarn run build && yarn run cli -- serve", "example:start:aot": "yarn run build && yarn run cli -- serve --aot", "example:test": "yarn run cli -- test --code-coverage", + "example:build:prod": "yarn build && yarn cli -- build --aot -prod --base-href \"/platform/example-app/\" --output-path \"./example-dist/example-app\"", "ci": "yarn run build && yarn run test && nyc report --reporter=text-lcov | coveralls", "prettier": "prettier --parser typescript --single-quote --trailing-comma es5 --write \"./**/*.ts\"", "watch:tests": "chokidar 'modules/**/*.ts' --initial -c 'nyc --reporter=text --reporter=html yarn run test:unit'", @@ -23,30 +24,16 @@ "release": "lerna publish --skip-npm --conventional-commits && npm run build" }, "lint-staged": { - "*.ts": [ - "yarn prettier", - "git add" - ] + "*.ts": ["yarn prettier", "git add"] }, - "keywords": [ - "ngrx", - "angular", - "rxjs" - ], + "keywords": ["ngrx", "angular", "rxjs"], "author": "Rob Wormald ", "license": "MIT", "repository": {}, "nyc": { - "extension": [ - ".ts" - ], - "exclude": [ - "**/*.spec", - "**/spec/**/*" - ], - "include": [ - "**/*.ts" - ] + "extension": [".ts"], + "exclude": ["**/*.spec", "**/spec/**/*"], + "include": ["**/*.ts"] }, "devDependencies": { "@angular/animations": "^4.2.0", @@ -120,4 +107,4 @@ "url": "https://opencollective.com/ngrx", "logo": "https://opencollective.com/opencollective/logo.txt" } -} \ No newline at end of file +} From 618bdf3293296a44d10a1a03e13dc987f477a708 Mon Sep 17 00:00:00 2001 From: Konrad Garus Date: Fri, 1 Sep 2017 01:05:25 +0100 Subject: [PATCH 50/67] chore(docs): Add header for "usage" in router-store readme (#337) --- docs/router-store/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/router-store/README.md b/docs/router-store/README.md index 6bb08e2239..a3907c2366 100644 --- a/docs/router-store/README.md +++ b/docs/router-store/README.md @@ -12,6 +12,8 @@ Install @ngrx/router-store from npm: `npm install github:ngrx/router-store-builds` OR `yarn add github:ngrx/router-store-builds` +## Usage + During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action, which has the following signature: ```ts From 870a73d9e7f08dca72cf61fd15fdee7958c66dfb Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 31 Aug 2017 21:20:26 -0500 Subject: [PATCH 51/67] chore(docs): Fix import for EffectNotification (#339) --- docs/effects/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/effects/api.md b/docs/effects/api.md index 1f618cb581..3e8c9f12cd 100644 --- a/docs/effects/api.md +++ b/docs/effects/api.md @@ -107,7 +107,7 @@ import 'rxjs/add/operator/takeUntil'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Action } from '@ngrx/store'; -import { Actions, Effect, OnRunEffects, EffectsNotification } from '@ngrx/effects'; +import { Actions, Effect, OnRunEffects, EffectNotification } from '@ngrx/effects'; @Injectable() export class UserEffects implements OnRunEffects { @@ -119,7 +119,7 @@ export class UserEffects implements OnRunEffects { console.log(action); }); - ngrxOnRunEffects(resolvedEffects$: Observable) { + ngrxOnRunEffects(resolvedEffects$: Observable) { return this.actions$.ofType('LOGGED_IN') .exhaustMap(() => resolvedEffects$.takeUntil('LOGGED_OUT')); } From a82f675bfbc24dbe5cc0815804c151384d275214 Mon Sep 17 00:00:00 2001 From: Laurent Goudet Date: Fri, 1 Sep 2017 15:09:26 +0200 Subject: [PATCH 52/67] chore(docs): Add meta reducers injection in the Store docs (#341) --- docs/store/README.md | 1 + docs/store/api.md | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/store/README.md b/docs/store/README.md index 3f90a4d38e..e9949299a0 100644 --- a/docs/store/README.md +++ b/docs/store/README.md @@ -117,6 +117,7 @@ export class MyAppComponent { - [Action Reducers](./actions.md#action-reducers) - [Injecting reducers](./api.md#injecting-reducers) - [Meta-Reducers/Enhancers](./api.md#meta-reducers) +- [Injecting Meta-Reducers](./api.md#injecting-meta-reducers) - [Providing initial state](./api.md#initial-state) - [State composition through feature modules](./api.md#feature-module-state-composition) - [State selectors](./selectors.md) diff --git a/docs/store/api.md b/docs/store/api.md index 9143cedbfe..f855d583c4 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -158,3 +158,29 @@ export const reducers: ActionReducerMap = { }) export class FeatureModule { } ``` + +## Injecting Meta-Reducers + +To inject meta reducers, use the `META_REDUCERS` injection token exported in +the Store API and a `Provider` to register the meta reducers through dependency +injection. + +``` +import { MetaReducer, META_REDUCERS } '@ngrx/store'; +import { SomeService } from './some.service'; + +export function getMetaReducers(some: SomeService): MetaReducer[] { + // return array of meta reducers; +} + +@NgModule({ + providers: [ + { + provide: META_REDUCERS, + deps: [SomeService], + useFactory: getMetaReducers + } + ] +}) +export class AppModule {} +``` From 2f6a0357bdfff60d2bc5c558563538d68aeb61e0 Mon Sep 17 00:00:00 2001 From: tdeschryver Date: Sat, 2 Sep 2017 16:36:28 +0200 Subject: [PATCH 53/67] feat(Store): createSelector with an array of selectors (#340) Closes #192 --- modules/store/spec/selector.spec.ts | 86 +++++++++++++++++++++++++++++ modules/store/src/selector.ts | 84 +++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index 68aefe0635..5862b19c95 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -119,6 +119,92 @@ describe('Selectors', () => { }); }); + describe('createSelector with arrays', () => { + it('should deliver the value of selectors to the projection function', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector([incrementOne, incrementTwo], projectFn)( + {} + ); + + expect(projectFn).toHaveBeenCalledWith(countOne, countTwo); + }); + + it('should be possible to test a projector fn independent from the selectors it is composed of', () => { + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector([incrementOne, incrementTwo], projectFn); + + selector.projector('', ''); + + expect(incrementOne).not.toHaveBeenCalled(); + expect(incrementTwo).not.toHaveBeenCalled(); + expect(projectFn).toHaveBeenCalledWith('', ''); + }); + + it('should call the projector function only when the value of a dependent selector change', () => { + const firstState = { first: 'state', unchanged: 'state' }; + const secondState = { second: 'state', unchanged: 'state' }; + const neverChangingSelector = jasmine + .createSpy('unchangedSelector') + .and.callFake((state: any) => { + return state.unchanged; + }); + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector([neverChangingSelector], projectFn); + + selector(firstState); + selector(secondState); + + expect(projectFn).toHaveBeenCalledTimes(1); + }); + + it('should memoize the function', () => { + const firstState = { first: 'state' }; + const secondState = { second: 'state' }; + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector( + [incrementOne, incrementTwo, incrementThree], + projectFn + ); + + selector(firstState); + selector(firstState); + selector(firstState); + selector(secondState); + + expect(incrementOne).toHaveBeenCalledTimes(2); + expect(incrementTwo).toHaveBeenCalledTimes(2); + expect(incrementThree).toHaveBeenCalledTimes(2); + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should allow you to release memoized arguments', () => { + const state = { first: 'state' }; + const projectFn = jasmine.createSpy('projectionFn'); + const selector = createSelector([incrementOne], projectFn); + + selector(state); + selector(state); + selector.release(); + selector(state); + selector(state); + + expect(projectFn).toHaveBeenCalledTimes(2); + }); + + it('should recursively release ancestor selectors', () => { + const grandparent = createSelector([incrementOne], a => a); + const parent = createSelector([grandparent], a => a); + const child = createSelector([parent], a => a); + spyOn(grandparent, 'release').and.callThrough(); + spyOn(parent, 'release').and.callThrough(); + + child.release(); + + expect(grandparent.release).toHaveBeenCalled(); + expect(parent.release).toHaveBeenCalled(); + }); + }); + describe('createFeatureSelector', () => { let featureName = '@ngrx/router-store'; let featureSelector: (state: any) => number; diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index 8f42733dd3..5aaefebb65 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -43,17 +43,29 @@ export function createSelector( s1: Selector, projector: (S1: S1) => Result ): MemoizedSelector; +export function createSelector( + selectors: [Selector], + projector: (s1: S1) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, projector: (s1: S1, s2: S2) => Result ): MemoizedSelector; +export function createSelector( + selectors: [Selector, Selector], + projector: (s1: S1, s2: S2) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, s3: Selector, projector: (s1: S1, s2: S2, s3: S3) => Result ): MemoizedSelector; +export function createSelector( + selectors: [Selector, Selector, Selector], + projector: (s1: S1, s2: S2, s3: S3) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, @@ -61,6 +73,15 @@ export function createSelector( s4: Selector, projector: (s1: S1, s2: S2, s3: S3, s4: S4) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + Selector, + Selector, + Selector, + Selector + ], + projector: (s1: S1, s2: S2, s3: S3, s4: S4) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, @@ -69,6 +90,16 @@ export function createSelector( s5: Selector, projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + Selector, + Selector, + Selector, + Selector, + Selector + ], + projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, @@ -78,6 +109,17 @@ export function createSelector( s6: Selector, projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + Selector, + Selector, + Selector, + Selector, + Selector, + Selector + ], + projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, @@ -88,6 +130,18 @@ export function createSelector( s7: Selector, projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result ): MemoizedSelector; +export function createSelector( + selectors: [ + Selector, + Selector, + Selector, + Selector, + Selector, + Selector, + Selector + ], + projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result +): MemoizedSelector; export function createSelector( s1: Selector, s2: Selector, @@ -108,7 +162,35 @@ export function createSelector( s8: S8 ) => Result ): MemoizedSelector; -export function createSelector(...args: any[]): Selector { +export function createSelector( + selectors: [ + Selector, + Selector, + Selector, + Selector, + Selector, + Selector, + Selector, + Selector + ], + projector: ( + s1: S1, + s2: S2, + s3: S3, + s4: S4, + s5: S5, + s6: S6, + s7: S7, + s8: S8 + ) => Result +): MemoizedSelector; +export function createSelector(...input: any[]): Selector { + let args = input; + if (Array.isArray(args[0])) { + const [head, ...tail] = args; + args = [...head, ...tail]; + } + const selectors = args.slice(0, args.length - 1); const projector = args[args.length - 1]; const memoizedSelectors = selectors.filter( From 28e1983bfd53639afd91336a5c37a87d7f5684d1 Mon Sep 17 00:00:00 2001 From: David Herges Date: Tue, 5 Sep 2017 03:23:36 +0200 Subject: [PATCH 54/67] docs(RouterStore): Fix type signature of ROUTER_NAVIGATION action (#344) --- docs/router-store/README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/router-store/README.md b/docs/router-store/README.md index a3907c2366..4574848bc0 100644 --- a/docs/router-store/README.md +++ b/docs/router-store/README.md @@ -14,13 +14,23 @@ Install @ngrx/router-store from npm: ## Usage -During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action, which has the following signature: +During the navigation, before any guards or resolvers run, the router will dispatch a `ROUTER_NAVIGATION` action, which has the signature `RouterNavigationAction`: ```ts -export type RouterNavigationPayload = { - routerState: T, - event: RoutesRecognized -} +/** + * Payload of ROUTER_NAVIGATION. + */ +export declare type RouterNavigationPayload = { + routerState: T; + event: RoutesRecognized; +}; +/** + * An action dispatched when the router navigates. + */ +export declare type RouterNavigationAction = { + type: typeof ROUTER_NAVIGATION; + payload: RouterNavigationPayload; +}; ``` - Reducers receive this action. Throwing an error in the reducer cancels navigation. From 920c0bab702d6bc99eb74c53c9c0a2eacebb5132 Mon Sep 17 00:00:00 2001 From: Pusztai Tibor Date: Thu, 7 Sep 2017 03:58:40 +0200 Subject: [PATCH 55/67] fix(RouterStore): Fix cancelled navigation with async guard (fixes #354) (#355) Closes #354 #201 --- modules/router-store/spec/integration.spec.ts | 35 +++++++++++++++++++ .../router-store/src/router_store_module.ts | 2 ++ 2 files changed, 37 insertions(+) diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index 4af94dff08..9e0519a881 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -14,7 +14,10 @@ import { } from '../src/index'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/first'; +import 'rxjs/add/operator/mapTo'; +import 'rxjs/add/operator/take'; import 'rxjs/add/operator/toPromise'; +import { of } from 'rxjs/observable/of'; describe('integration spec', () => { it('should work', done => { @@ -370,6 +373,38 @@ describe('integration spec', () => { }); }); }); + + it('should support event during an async canActivate guard', done => { + createTestModule({ + reducers: { routerReducer }, + canActivate: () => { + store.dispatch({ type: 'USER_EVENT' }); + return store.take(1).mapTo(true); + }, + }); + + const router: Router = TestBed.get(Router); + const store: Store = TestBed.get(Store); + const log = logOfRouterAndStore(router, store); + + router + .navigateByUrl('/') + .then(() => { + log.splice(0); + return router.navigateByUrl('next'); + }) + .then(() => { + expect(log).toEqual([ + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'store', state: undefined }, // after ROUTER_NAVIGATION + { type: 'store', state: undefined }, // after USER_EVENT + { type: 'router', event: 'NavigationEnd', url: '/next' }, + ]); + + done(); + }); + }); }); function createTestModule( diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index cbf111c2be..7343f5758d 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -186,6 +186,8 @@ export class StoreRouterConnectingModule { private setUpStoreStateListener(): void { this.store.subscribe(s => { this.storeState = s; + }); + this.store.select('routerReducer').subscribe(() => { this.navigateIfNeeded(); }); } From 127ccc99663442ea1a902e459ae9fb1041fd7f80 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 7 Sep 2017 16:14:18 -0500 Subject: [PATCH 56/67] chore(Example): Added ngrx-store-freeze meta-reducer to example application (#343) --- example-app/app/reducers/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/example-app/app/reducers/index.ts b/example-app/app/reducers/index.ts index 86b13db736..6f83242f5c 100644 --- a/example-app/app/reducers/index.ts +++ b/example-app/app/reducers/index.ts @@ -9,6 +9,13 @@ import { environment } from '../../environments/environment'; import { RouterStateUrl } from '../shared/utils'; import * as fromRouter from '@ngrx/router-store'; +/** + * storeFreeze prevents state from being mutated. When mutation occurs, an + * exception will be thrown. This is useful during development mode to + * ensure that none of the reducers accidentally mutates the state. + */ +import { storeFreeze } from 'ngrx-store-freeze'; + /** * Every reducer module's default export is the reducer function itself. In * addition, each module should export a type or interface that describes @@ -53,7 +60,7 @@ export function logger(reducer: ActionReducer): ActionReducer { * that will be composed to form the root meta-reducer. */ export const metaReducers: MetaReducer[] = !environment.production - ? [logger] + ? [logger, storeFreeze] : []; /** From 81afd0d71c0aca6d051a88954e2fa4edbf9a9cf2 Mon Sep 17 00:00:00 2001 From: Emil Abraham Date: Thu, 7 Sep 2017 19:03:29 -0400 Subject: [PATCH 57/67] chore(docs): Fix action interface example (#360) --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 0799c6eb5b..06f63a2c9c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -92,7 +92,7 @@ export interface ActionWithPayload extends Action { And if you need an unsafe version to help with transition. ```ts -export interface UnsafeAction implements Action { +export interface UnsafeAction extends Action { payload?: any; } ``` From 0528d2ddea5a0a772d7130f7296984e82369961a Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 8 Sep 2017 19:05:19 -0500 Subject: [PATCH 58/67] fix(RouterStore): Stringify error from navigation error event (#357) Closes #356 --- modules/router-store/src/router_store_module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 7343f5758d..b65a5990b3 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -248,7 +248,7 @@ export class StoreRouterConnectingModule { this.dispatchRouterAction(ROUTER_ERROR, { routerState: this.routerState, storeState: this.storeState, - event, + event: new NavigationError(event.id, event.url, `${event}`), }); } From 0f007798225b7718a9dae68a3d5ce2883e42c897 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 8 Sep 2017 22:35:12 -0500 Subject: [PATCH 59/67] chore(Example): Documented @ngrx/entity and added to example app (#328) --- docs/entity/README.md | 24 ++ docs/entity/adapter.md | 297 +++++++++++++++++++ docs/entity/interfaces.md | 37 +++ example-app/app/books/models/book.ts | 20 ++ example-app/app/books/reducers/book.spec.ts | 165 ----------- example-app/app/books/reducers/books.spec.ts | 127 ++++++++ example-app/app/books/reducers/books.ts | 95 +++--- example-app/app/books/reducers/collection.ts | 29 +- example-app/app/books/reducers/index.ts | 34 ++- example-app/app/books/reducers/search.ts | 9 +- example-app/tsconfig.app.json | 4 +- example-app/tsconfig.spec.json | 4 +- tsconfig.json | 3 + 13 files changed, 599 insertions(+), 249 deletions(-) create mode 100644 docs/entity/README.md create mode 100644 docs/entity/adapter.md create mode 100644 docs/entity/interfaces.md delete mode 100644 example-app/app/books/reducers/book.spec.ts create mode 100644 example-app/app/books/reducers/books.spec.ts diff --git a/docs/entity/README.md b/docs/entity/README.md new file mode 100644 index 0000000000..1780c537fd --- /dev/null +++ b/docs/entity/README.md @@ -0,0 +1,24 @@ +# @ngrx/entity + +Entity State adapter for managing record collections. + +@ngrx/entity provides an API to manipulate and query entity collections. + +- Reduces boilerplate for managing common datasets +- Provides performant operations for managing entity collections +- Extensible type-safe adapters for selecting entity information + +### Installation +Install @ngrx/entity from npm: + +`npm install @ngrx/entity --save` OR `yarn add @ngrx/entity` + + +### Nightly builds + +`npm install github:ngrx/entity-builds` OR `yarn add github:ngrx/entity-builds` + +## API Documentation +- [Interfaces](./interfaces.md) +- [Entity Adapter](./adapter.md) +- [Selectors]('./adapter.md#entity-selectors) diff --git a/docs/entity/adapter.md b/docs/entity/adapter.md new file mode 100644 index 0000000000..2a247bbbab --- /dev/null +++ b/docs/entity/adapter.md @@ -0,0 +1,297 @@ +# Entity Adapter + +## createEntityAdapter + +A method for returning a generic entity adapter for a single entity state collection. The +returned adapter provides many [methods](#adapter-methods) for performing operations +against the collection type. The method takes an object for configuration with 2 properties. + + - `selectId`: A `method` for selecting the primary id for the collection + - `sort`: A [sort function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) for sorting the collection. Set to `false` to leave collection unsorted. + +Usage: + +```ts +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number; +} + +export function sortByName(a: User, b: User): number { + return a.name.localeCompare(b.name); +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id, + sort: sortByName, +}); +``` + +## Adapter Methods + +These methods are provided by the adapter object returned +when using [createEntityAdapter](#createEntityAdapter). The methods are used inside your reducer function to manage +the entity collection based on your provided actions. + +### getInitialState + +Returns the `initialState` for entity state based on the provided type. Additional state is also provided through the provided configuration object. The initialState is provided to your reducer function. + +Usage: + +```ts +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number | null; +} + +export const initialState: State = adapter.getInitialState({ + // additional entity state properties + selectedUserId: null +}); + +export function reducer(state = initialState, action): State { + switch(action.type) { + default: { + return state; + } + } +} +``` + +## Adapter Collection Methods + +The entity adapter also provides methods for operations against an entity. These methods can change +one to many records at a time. Each method returns the newly modified state if changes were made and the same +state if no changes were made. + +* `addOne`: Add one entity to the collection +* `addMany`: Add multiple entities to the collection +* `addAll`: Replace current collection with provided collection +* `removeOne`: Remove one entity to the collection +* `removeMany`: Remove multiple entities to the collection +* `removeAll`: Clear entity collection +* `updateOne`: Update one entity in the collection +* `updateMany`: Update multiple entities in the collection + +Usage: + +`user.model.ts` + +```ts +export interface User { + id: number; + name: string; + description: string; +} +``` + +`user.actions.ts` + +```ts +import { Action } from '@ngrx/action'; +import { User } from './user.model'; + +export const LOAD_USERS = '[User] Load Users'; +export const ADD_USER = '[User] Add User'; +export const ADD_USERS = '[User] Add Users'; +export const UPDATE_USER = '[User] Update User'; +export const UPDATE_USERS = '[User] Update Users'; +export const DELETE_USER = '[User] Delete User'; +export const DELETE_USERS = '[User] Delete Users'; +export const CLEAR_USERS = '[User] Clear Users'; + +export class LoadUsers implements Action { + readonly type = LOAD_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class AddUser implements Action { + readonly type = ADD_USER; + + constructor(public payload: { user: User }) {} +} + +export class AddUsers implements Action { + readonly type = ADD_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class UpdateUser implements Action { + readonly type = UPDATE_USER; + + constructor(public payload: { user: User }) {} +} + +export class UpdateUsers implements Action { + readonly type = UPDATE_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class DeleteUser implements Action { + readonly type = DELETE_USER; + + constructor(public payload: { user: User }) {} +} + +export class DeleteUsers implements Action { + readonly type = DELETE_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class ClearUsers implements Action { + readonly type = CLEAR_USERS; +} + +export type All = + LoadUsers + | AddUser + | AddUsers + | UpdateUser + | UpdateUsers + | DeleteUser + | DeleteUsers + | ClearUsers; +``` + +`user.reducer.ts` +```ts +import * as user from './user.actions'; + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number | null; +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id, + sort: true, +}); + +export const initialState: State = adapter.getInitialState({ + // additional entity state properties + selectedUserId: null +}); + +export function reducer( + state = initialState, + action: UserActions.All +): State { + switch (action.type) { + case user.ADD_USER: { + return { + ...state, + ...adapter.addOne(action.payload.user, state), + }; + } + + case user.ADD_USERS: { + return { + ...state, + ...adapter.addMany(action.payload.users, state), + }; + } + + case user.UPDATE_USER: { + return { + ...state, + ...adapter.updateOne(action.payload.user, state), + }; + } + + case user.UPDATE_USERS: { + return { + ...state, + ...adapter.updateMany(action.payload.users, state), + }; + } + + case user.LOAD_USERS: { + return { + ...state, + ...adapter.addAll(action.payload.users, state), + }; + } + + case user.CLEAR_USERS: { + return { + ...adapter.removeAll(state), + selectedUserId: null + }; + } + + default: { + return state; + } + } +} + +export const getSelectedUserId = (state: State) => state.selectedUserId; +``` + +### Entity Selectors + +The `getSelectors` method returned by the created entity adapter provides functions for selecting information from the entity. + +The `getSelectors` method takes a selector function +as its only argument to select the piece of state for a defined entity. + +Usage: + +`reducers/index.ts` + +```ts +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromUser from './user.reducer'; + +export interface State { + users: fromUser.State; +} + +export const selectUserState = createFeatureSelector('users'); + +export const { + // select the array of user ids + selectIds: getUserIds, + + // select the dictionary of user entities + selectEntities: getUserEntities, + + // select the array of users + selectAll: getAllUsers, + + // select the total user count + selectTotal: getUserTotal +} = fromUser.adapter.getSelectors(selectUserState); + +export const selectUserIds = createSelector(selectUserState, getUserIds); +export const selectUserEntities = createSelector(selectUserState, getUserEntities); +export const selectAllUsers = createSelector(selectUserState, getAllUsers); +export const selectUserCount = createSelector(selectUserState, getUserTotal); +export const selectCurrentUserId = createSelector(selectUserState, fromUser.getSelectedUserId); +export const selectCurrentUser = createSelector( + selectUserEntities, + selectCurrentUserId, + (userEntities, userId) => userEntities[userId] +); +``` diff --git a/docs/entity/interfaces.md b/docs/entity/interfaces.md new file mode 100644 index 0000000000..55294543ea --- /dev/null +++ b/docs/entity/interfaces.md @@ -0,0 +1,37 @@ +# Entity Interfaces + +## EntityState + +The Entity State is a predefined generic interface for a given entity collection with the following properties: + + * `ids`: An array of all the primary ids in the collection + * `entities`: A dictionary of entities in the collection indexed by the primary id + + Extend this interface to provided any additional properties for the entity state. + + Usage: + + ```ts + export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entity state properties + selectedUserId: string | null; +} +``` + +## EntityAdapter + +Provides a generic type interface for the provided [entity adapter](./adapter#createEntityAdapter). The entity adapter provides many [collection methods](./adapter.md#adapter-collection-methods) for managing the entity state. + +Usage: + +```ts +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id +}); +``` diff --git a/example-app/app/books/models/book.ts b/example-app/app/books/models/book.ts index 70cf4e623a..c8b067ac12 100644 --- a/example-app/app/books/models/book.ts +++ b/example-app/app/books/models/book.ts @@ -15,3 +15,23 @@ export interface Book { }; }; } + +export function generateMockBook(): Book { + return { + id: '1', + volumeInfo: { + title: 'title', + subtitle: 'subtitle', + authors: ['author'], + publisher: 'publisher', + publishDate: '', + description: 'description', + averageRating: 3, + ratingsCount: 5, + imageLinks: { + thumbnail: 'string', + smallThumbnail: 'string', + }, + }, + }; +} diff --git a/example-app/app/books/reducers/book.spec.ts b/example-app/app/books/reducers/book.spec.ts deleted file mode 100644 index 45650f4119..0000000000 --- a/example-app/app/books/reducers/book.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { reducer } from './books'; -import * as fromBooks from './books'; -import { SearchComplete, Load, Select } from '../actions/book'; -import { Book } from '../models/book'; -import { LoadSuccess } from '../actions/collection'; - -describe('BooksReducer', () => { - describe('undefined action', () => { - it('should return the default state', () => { - const action = {} as any; - - const result = reducer(undefined, action); - expect(result).toEqual(fromBooks.initialState); - }); - }); - - describe('SEARCH_COMPLETE & LOAD_SUCCESS', () => { - function noExistingBooks(action: any) { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const createAction = new action([book1, book2]); - - const expectedResult = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: null, - }; - - const result = reducer(fromBooks.initialState, createAction); - expect(result).toEqual(expectedResult); - } - - function existingBooks(action: any) { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const initialState = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: null, - } as any; - // should not replace existing books - const differentBook2 = { id: '222', foo: 'bar' } as any; - const book3 = { id: '333' } as Book; - const createAction = new action([book3, differentBook2]); - - const expectedResult = { - ids: ['111', '222', '333'], - entities: { - '111': book1, - '222': book2, - '333': book3, - }, - selectedBookId: null, - }; - - const result = reducer(initialState, createAction); - expect(result).toEqual(expectedResult); - } - - it('should add all books in the payload when none exist', () => { - noExistingBooks(SearchComplete); - noExistingBooks(LoadSuccess); - }); - - it('should add only new books when books already exist', () => { - existingBooks(SearchComplete); - existingBooks(LoadSuccess); - }); - }); - - describe('LOAD', () => { - it('should add a single book, if the book does not exist', () => { - const book = { id: '888' } as Book; - const action = new Load(book); - - const expectedResult = { - ids: ['888'], - entities: { - '888': book, - }, - selectedBookId: null, - }; - - const result = reducer(fromBooks.initialState, action); - expect(result).toEqual(expectedResult); - }); - - it('should return the existing state if the book exists', () => { - const initialState = { - ids: ['999'], - entities: { - '999': { id: '999' }, - }, - } as any; - const book = { id: '999', foo: 'baz' } as any; - const action = new Load(book); - - const result = reducer(initialState, action); - expect(result).toEqual(initialState); - }); - }); - - describe('SELECT', () => { - it('should set the selected book id on the state', () => { - const action = new Select('1'); - - const result = reducer(fromBooks.initialState, action); - expect(result.selectedBookId).toBe('1'); - }); - }); - - describe('Selections', () => { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const state: fromBooks.State = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: '111', - }; - - describe('getEntities', () => { - it('should return entities', () => { - const result = fromBooks.getEntities(state); - expect(result).toBe(state.entities); - }); - }); - - describe('getIds', () => { - it('should return ids', () => { - const result = fromBooks.getIds(state); - expect(result).toBe(state.ids); - }); - }); - - describe('getSelectedId', () => { - it('should return the selected id', () => { - const result = fromBooks.getSelectedId(state); - expect(result).toBe('111'); - }); - }); - - describe('getSelected', () => { - it('should return the selected book', () => { - const result = fromBooks.getSelected(state); - expect(result).toBe(book1); - }); - }); - - describe('getAll', () => { - it('should return all books as an array ', () => { - const result = fromBooks.getAll(state); - expect(result).toEqual([book1, book2]); - }); - }); - }); -}); diff --git a/example-app/app/books/reducers/books.spec.ts b/example-app/app/books/reducers/books.spec.ts new file mode 100644 index 0000000000..47560daa80 --- /dev/null +++ b/example-app/app/books/reducers/books.spec.ts @@ -0,0 +1,127 @@ +import { reducer } from './books'; +import * as fromBooks from './books'; +import { SearchComplete, Load, Select } from '../actions/book'; +import { Book, generateMockBook } from '../models/book'; +import { LoadSuccess } from '../actions/collection'; + +describe('BooksReducer', () => { + const book1 = generateMockBook(); + const book2 = { ...book1, id: '222' }; + const book3 = { ...book1, id: '333' }; + const initialState: fromBooks.State = { + ids: [book1.id, book2.id], + entities: { + [book1.id]: book1, + [book2.id]: book2, + }, + selectedBookId: null, + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(fromBooks.initialState); + }); + }); + + describe('SEARCH_COMPLETE & LOAD_SUCCESS', () => { + function noExistingBooks( + action: any, + booksInitialState: any, + initialState: any, + books: Book[] + ) { + const createAction = new action(books); + + const result = reducer(booksInitialState, createAction); + + expect(result).toEqual(initialState); + } + + function existingBooks(action: any, initialState: any, books: Book[]) { + // should not replace existing books + const differentBook2 = { ...books[0], foo: 'bar' }; + const createAction = new action([books[1], differentBook2]); + + const expectedResult = { + ids: [...initialState.ids, books[1].id], + entities: { + ...initialState.entities, + [books[1].id]: books[1], + }, + selectedBookId: null, + }; + + const result = reducer(initialState, createAction); + expect(result).toEqual(expectedResult); + } + + it('should add all books in the payload when none exist', () => { + noExistingBooks(SearchComplete, fromBooks.initialState, initialState, [ + book1, + book2, + ]); + + noExistingBooks(LoadSuccess, fromBooks.initialState, initialState, [ + book1, + book2, + ]); + }); + + it('should add only new books when books already exist', () => { + existingBooks(SearchComplete, initialState, [book2, book3]); + + existingBooks(LoadSuccess, initialState, [book2, book3]); + }); + }); + + describe('LOAD', () => { + const expectedResult = { + ids: [book1.id], + entities: { + [book1.id]: book1, + }, + selectedBookId: null, + }; + + it('should add a single book, if the book does not exist', () => { + const action = new Load(book1); + + const result = reducer(fromBooks.initialState, action); + + expect(result).toEqual(expectedResult); + }); + + it('should return the existing state if the book exists', () => { + const action = new Load(book1); + + const result = reducer(expectedResult, action); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('SELECT', () => { + it('should set the selected book id on the state', () => { + const action = new Select(book1.id); + + const result = reducer(initialState, action); + + expect(result.selectedBookId).toBe(book1.id); + }); + }); + + describe('Selectors', () => { + describe('getSelectedId', () => { + it('should return the selected id', () => { + const result = fromBooks.getSelectedId({ + ...initialState, + selectedBookId: book1.id, + }); + + expect(result).toBe(book1.id); + }); + }); + }); +}); diff --git a/example-app/app/books/reducers/books.ts b/example-app/app/books/reducers/books.ts index b1a91c9fea..589ba961fd 100644 --- a/example-app/app/books/reducers/books.ts +++ b/example-app/app/books/reducers/books.ts @@ -1,19 +1,40 @@ import { createSelector } from '@ngrx/store'; +import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { Book } from '../models/book'; import * as book from '../actions/book'; import * as collection from '../actions/collection'; -export interface State { - ids: string[]; - entities: { [id: string]: Book }; +/** + * @ngrx/entity provides a predefined interface for handling + * a structured dictionary of records. This interface + * includes an array of ids, and a dictionary of the provided + * model type by id. This interface is extended to include + * any additional interface properties. + */ +export interface State extends EntityState { selectedBookId: string | null; } -export const initialState: State = { - ids: [], - entities: {}, +/** + * createEntityAdapter creates many an object of helper + * functions for single or multiple operations + * against the dictionary of records. The configuration + * object takes a record id selector function and + * a sort option whether to sort the records when performing + * operations + */ +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (book: Book) => book.id, + sort: false, +}); + +/** getInitialState returns the default initial state + * for the generated entity state. Initial state + * additional properties can also be defined. +*/ +export const initialState: State = adapter.getInitialState({ selectedBookId: null, -}; +}); export function reducer( state = initialState, @@ -22,46 +43,36 @@ export function reducer( switch (action.type) { case book.SEARCH_COMPLETE: case collection.LOAD_SUCCESS: { - const books = action.payload; - const newBooks = books.filter(book => !state.entities[book.id]); - - const newBookIds = newBooks.map(book => book.id); - const newBookEntities = newBooks.reduce( - (entities: { [id: string]: Book }, book: Book) => { - return Object.assign(entities, { - [book.id]: book, - }); - }, - {} - ); - return { - ids: [...state.ids, ...newBookIds], - entities: Object.assign({}, state.entities, newBookEntities), + /** + * The addMany function provided by the created adapter + * adds many records to the entity dictionary + * and returns a new state including those records. If + * the collection is to be sorted, the adapter will + * sort each record upon entry into the sorted array. + */ + ...adapter.addMany(action.payload, state), selectedBookId: state.selectedBookId, }; } case book.LOAD: { - const book = action.payload; - - if (state.ids.indexOf(book.id) > -1) { - return state; - } - return { - ids: [...state.ids, book.id], - entities: Object.assign({}, state.entities, { - [book.id]: book, - }), + /** + * The addOne function provided by the created adapter + * adds one record to the entity dictionary + * and returns a new state including that records if it doesn't + * exist already. If the collection is to be sorted, the adapter will + * insert the new record into the sorted array. + */ + ...adapter.addOne(action.payload, state), selectedBookId: state.selectedBookId, }; } case book.SELECT: { return { - ids: state.ids, - entities: state.entities, + ...state, selectedBookId: action.payload, }; } @@ -81,20 +92,4 @@ export function reducer( * use-case. */ -export const getEntities = (state: State) => state.entities; - -export const getIds = (state: State) => state.ids; - export const getSelectedId = (state: State) => state.selectedBookId; - -export const getSelected = createSelector( - getEntities, - getSelectedId, - (entities, selectedId) => { - return entities[selectedId]; - } -); - -export const getAll = createSelector(getEntities, getIds, (entities, ids) => { - return ids.map(id => entities[id]); -}); diff --git a/example-app/app/books/reducers/collection.ts b/example-app/app/books/reducers/collection.ts index f8f65f74b1..cb2d64eb4c 100644 --- a/example-app/app/books/reducers/collection.ts +++ b/example-app/app/books/reducers/collection.ts @@ -18,41 +18,38 @@ export function reducer( ): State { switch (action.type) { case collection.LOAD: { - return Object.assign({}, state, { + return { + ...state, loading: true, - }); + }; } case collection.LOAD_SUCCESS: { - const books = action.payload; - return { loaded: true, loading: false, - ids: books.map(book => book.id), + ids: action.payload.map(book => book.id), }; } case collection.ADD_BOOK_SUCCESS: case collection.REMOVE_BOOK_FAIL: { - const book = action.payload; - - if (state.ids.indexOf(book.id) > -1) { + if (state.ids.indexOf(action.payload.id) > -1) { return state; } - return Object.assign({}, state, { - ids: [...state.ids, book.id], - }); + return { + ...state, + ids: [...state.ids, action.payload.id], + }; } case collection.REMOVE_BOOK_SUCCESS: case collection.ADD_BOOK_FAIL: { - const book = action.payload; - - return Object.assign({}, state, { - ids: state.ids.filter(id => id !== book.id), - }); + return { + ...state, + ids: state.ids.filter(id => id !== action.payload.id), + }; } default: { diff --git a/example-app/app/books/reducers/index.ts b/example-app/app/books/reducers/index.ts index c273977c6b..3ca7970199 100644 --- a/example-app/app/books/reducers/index.ts +++ b/example-app/app/books/reducers/index.ts @@ -53,23 +53,35 @@ export const getBooksState = createFeatureSelector('books'); */ export const getBookEntitiesState = createSelector( getBooksState, - (state: BooksState) => state.books -); -export const getBookEntities = createSelector( - getBookEntitiesState, - fromBooks.getEntities -); -export const getBookIds = createSelector( - getBookEntitiesState, - fromBooks.getIds + state => state.books ); + export const getSelectedBookId = createSelector( getBookEntitiesState, fromBooks.getSelectedId ); + +/** + * Adapters created with @ngrx/entity generate + * commonly used selector functions including + * getting all ids in the record set, a dictionary + * of the records by id, an array of records and + * the total number of records. This reducers boilerplate + * in selecting records from the entity state. + */ +export const { + selectIds: getBookIds, + selectEntities: getBookEntities, + selectAll: getAllBooks, + selectTotal: getTotalBooks, +} = fromBooks.adapter.getSelectors(getBookEntitiesState); + export const getSelectedBook = createSelector( - getBookEntitiesState, - fromBooks.getSelected + getBookEntities, + getSelectedBookId, + (entities, selectedId) => { + return selectedId && entities[selectedId]; + } ); /** diff --git a/example-app/app/books/reducers/search.ts b/example-app/app/books/reducers/search.ts index 3840bde324..381285fd27 100644 --- a/example-app/app/books/reducers/search.ts +++ b/example-app/app/books/reducers/search.ts @@ -25,17 +25,16 @@ export function reducer(state = initialState, action: book.Actions): State { }; } - return Object.assign({}, state, { + return { + ...state, query, loading: true, - }); + }; } case book.SEARCH_COMPLETE: { - const books = action.payload; - return { - ids: books.map(book => book.id), + ids: action.payload.map(book => book.id), loading: false, query: state.query, }; diff --git a/example-app/tsconfig.app.json b/example-app/tsconfig.app.json index ee93846bfe..ef30cf72c9 100644 --- a/example-app/tsconfig.app.json +++ b/example-app/tsconfig.app.json @@ -17,9 +17,11 @@ "rootDir": "../", "paths": { "@ngrx/effects": ["../dist/effects"], + "@ngrx/effects/testing": ["../dist/effects/testing"], "@ngrx/store": ["../dist/store"], "@ngrx/router-store": ["../dist/router-store"], - "@ngrx/store-devtools": ["../dist/store-devtools"] + "@ngrx/store-devtools": ["../dist/store-devtools"], + "@ngrx/entity": ["../dist/entity"] } }, "exclude": [ diff --git a/example-app/tsconfig.spec.json b/example-app/tsconfig.spec.json index e4753f0407..19a8a96901 100644 --- a/example-app/tsconfig.spec.json +++ b/example-app/tsconfig.spec.json @@ -20,9 +20,11 @@ "rootDir": "../", "paths": { "@ngrx/effects": ["../modules/effects"], + "@ngrx/effects/testing": ["../modules/effects/testing"], "@ngrx/store": ["../modules/store"], "@ngrx/router-store": ["../modules/router-store"], - "@ngrx/store-devtools": ["../modules/store-devtools"] + "@ngrx/store-devtools": ["../modules/store-devtools"], + "@ngrx/entity": ["../modules/entity"] } }, "files": [ diff --git a/tsconfig.json b/tsconfig.json index 5bb3e217cd..b10bbc7884 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,9 @@ ], "@ngrx/router-store": [ "./modules/router-store" + ], + "@ngrx/entity": [ + "./modules/entity" ] } }, From 47f4ca84c3951f9cfc003b3591f995742f7a6718 Mon Sep 17 00:00:00 2001 From: Salem Ouerdani Date: Sat, 9 Sep 2017 20:26:00 +0100 Subject: [PATCH 60/67] docs(entity): Fix typo in url (#365) --- docs/entity/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/entity/README.md b/docs/entity/README.md index 1780c537fd..16eaebb895 100644 --- a/docs/entity/README.md +++ b/docs/entity/README.md @@ -21,4 +21,4 @@ Install @ngrx/entity from npm: ## API Documentation - [Interfaces](./interfaces.md) - [Entity Adapter](./adapter.md) -- [Selectors]('./adapter.md#entity-selectors) +- [Selectors](./adapter.md#entity-selectors) From 669b2f4ac82538abe2bb6111178175c0122e9a60 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 10 Sep 2017 21:11:27 -0500 Subject: [PATCH 61/67] chore(docs): Update @ngrx/entity documentation examples and usage (#369) Closes #367 --- .github/ISSUE_TEMPLATE.md | 1 - docs/entity/README.md | 6 ++--- docs/entity/adapter.md | 55 ++++++++++++++------------------------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index be0f04fe80..907359a703 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,7 +7,6 @@ [ ] Bug report [ ] Feature request [ ] Documentation issue or request -[ ] Support request ## What is the current behavior? diff --git a/docs/entity/README.md b/docs/entity/README.md index 16eaebb895..14c16bb93e 100644 --- a/docs/entity/README.md +++ b/docs/entity/README.md @@ -4,9 +4,9 @@ Entity State adapter for managing record collections. @ngrx/entity provides an API to manipulate and query entity collections. -- Reduces boilerplate for managing common datasets -- Provides performant operations for managing entity collections -- Extensible type-safe adapters for selecting entity information +- Reduces boilerplate for creating reducers that manage a collection of models. +- Provides performant CRUD operations for managing entity collections. +- Extensible type-safe adapters for selecting entity information. ### Installation Install @ngrx/entity from npm: diff --git a/docs/entity/adapter.md b/docs/entity/adapter.md index 2a247bbbab..a44e65edf4 100644 --- a/docs/entity/adapter.md +++ b/docs/entity/adapter.md @@ -7,7 +7,7 @@ returned adapter provides many [methods](#adapter-methods) for performing operat against the collection type. The method takes an object for configuration with 2 properties. - `selectId`: A `method` for selecting the primary id for the collection - - `sort`: A [sort function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) for sorting the collection. Set to `false` to leave collection unsorted. + - `sort`: A compare function used to [sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) the collection. The comparer function is only needed if the collection needs to be sorted before being displayed. Set to `false` to use leave the collection unsorted, which is more performant during CRUD operations. Usage: @@ -84,8 +84,8 @@ state if no changes were made. * `addOne`: Add one entity to the collection * `addMany`: Add multiple entities to the collection * `addAll`: Replace current collection with provided collection -* `removeOne`: Remove one entity to the collection -* `removeMany`: Remove multiple entities to the collection +* `removeOne`: Remove one entity from the collection +* `removeMany`: Remove multiple entities from the collection * `removeAll`: Clear entity collection * `updateOne`: Update one entity in the collection * `updateMany`: Update multiple entities in the collection @@ -176,7 +176,9 @@ export type All = `user.reducer.ts` ```ts -import * as user from './user.actions'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; +import { User } from './user.model'; +import * as UserActions from './user.actions'; export interface State extends EntityState { // additional entities state properties @@ -184,8 +186,7 @@ export interface State extends EntityState { } export const adapter: EntityAdapter = createEntityAdapter({ - selectId: (user: User) => user.id, - sort: true, + selectId: (user: User) => user.id }); export const initialState: State = adapter.getInitialState({ @@ -198,46 +199,28 @@ export function reducer( action: UserActions.All ): State { switch (action.type) { - case user.ADD_USER: { - return { - ...state, - ...adapter.addOne(action.payload.user, state), - }; + case UserActions.ADD_USER: { + return adapter.addOne(action.payload.user, state); } - case user.ADD_USERS: { - return { - ...state, - ...adapter.addMany(action.payload.users, state), - }; + case UserActions.ADD_USERS: { + return adapter.addMany(action.payload.users, state); } - case user.UPDATE_USER: { - return { - ...state, - ...adapter.updateOne(action.payload.user, state), - }; + case UserActions.UPDATE_USER: { + return adapter.updateOne(action.payload.user, state); } - case user.UPDATE_USERS: { - return { - ...state, - ...adapter.updateMany(action.payload.users, state), - }; + case UserActions.UPDATE_USERS: { + return adapter.updateMany(action.payload.users, state); } - case user.LOAD_USERS: { - return { - ...state, - ...adapter.addAll(action.payload.users, state), - }; + case UserActions.LOAD_USERS: { + return adapter.addAll(action.payload.users, state); } - case user.CLEAR_USERS: { - return { - ...adapter.removeAll(state), - selectedUserId: null - }; + case UserActions.CLEAR_USERS: { + return adapter.removeAll({ ...state, selectedUserId: null }); } default: { From 38b2f955e980c8f717a184e860fdda2db159018a Mon Sep 17 00:00:00 2001 From: Szpadel Date: Tue, 12 Sep 2017 03:19:29 +0200 Subject: [PATCH 62/67] fix(Store): Fix typing for feature to accept InjectionToken (#375) --- modules/store/src/store_module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index 0481dd2ad8..c6f3abbc95 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -144,17 +144,17 @@ export class StoreModule { static forFeature( featureName: string, - reducers: ActionReducerMap, + reducers: ActionReducerMap | InjectionToken>, config?: StoreConfig ): ModuleWithProviders; static forFeature( featureName: string, - reducer: ActionReducer, + reducer: ActionReducer| InjectionToken>, config?: StoreConfig ): ModuleWithProviders; static forFeature( featureName: string, - reducers: ActionReducerMap | ActionReducer, + reducers: ActionReducerMap | InjectionToken> | ActionReducer | InjectionToken>, config: StoreConfig = {} ): ModuleWithProviders { return { From a07c05e2e1cd38d0e580395bf2f183b702ca3294 Mon Sep 17 00:00:00 2001 From: Szpadel Date: Wed, 13 Sep 2017 15:43:55 +0200 Subject: [PATCH 63/67] chore(docs): Change providing injected reducers by factory instead of value (#387) --- docs/store/api.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/store/api.md b/docs/store/api.md index f855d583c4..0e99c3b53a 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -140,10 +140,12 @@ import * as fromFeature from './reducers'; export const FEATURE_REDUCER_TOKEN = new InjectionToken>('Feature Reducers'); -// map of reducers -export const reducers: ActionReducerMap = { - -}; +export function getReducers(): ActionReducerMap { + // map of reducers + return { + + }; +} @NgModule({ imports: [ @@ -152,7 +154,7 @@ export const reducers: ActionReducerMap = { providers: [ { provide: FEATURE_REDUCER_TOKEN, - useValue: reducers + useFactory: getReducers } ] }) From 9e1375d863d213e9d890b9caebb8ab8875fe4564 Mon Sep 17 00:00:00 2001 From: Philipp Bunge Date: Fri, 15 Sep 2017 01:22:52 +0200 Subject: [PATCH 64/67] chore(docs): Minor fixes to entity interfaces documentation (#390) - Fixes the type for the selectedUserId in the example. - Fixes the link to creating an entity adapter. --- docs/entity/interfaces.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entity/interfaces.md b/docs/entity/interfaces.md index 55294543ea..9a618dd771 100644 --- a/docs/entity/interfaces.md +++ b/docs/entity/interfaces.md @@ -20,13 +20,13 @@ The Entity State is a predefined generic interface for a given entity collection export interface State extends EntityState { // additional entity state properties - selectedUserId: string | null; + selectedUserId: number | null; } ``` ## EntityAdapter -Provides a generic type interface for the provided [entity adapter](./adapter#createEntityAdapter). The entity adapter provides many [collection methods](./adapter.md#adapter-collection-methods) for managing the entity state. +Provides a generic type interface for the provided [entity adapter](./adapter.md#createentityadapter). The entity adapter provides many [collection methods](./adapter.md#adapter-collection-methods) for managing the entity state. Usage: From 274554b34acd6e910e55c476e8852e0cad123b28 Mon Sep 17 00:00:00 2001 From: Daniel Karp Date: Sat, 16 Sep 2017 11:00:11 -0400 Subject: [PATCH 65/67] feat(Entity): Rename 'sort' to 'sortComparer' Closes #370 --- docs/entity/adapter.md | 4 ++-- example-app/app/books/reducers/books.ts | 6 +++--- modules/entity/spec/sorted_state_adapter.spec.ts | 2 +- modules/entity/src/create_adapter.ts | 11 +++++++---- modules/entity/src/models.ts | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/entity/adapter.md b/docs/entity/adapter.md index a44e65edf4..7cb325292a 100644 --- a/docs/entity/adapter.md +++ b/docs/entity/adapter.md @@ -7,7 +7,7 @@ returned adapter provides many [methods](#adapter-methods) for performing operat against the collection type. The method takes an object for configuration with 2 properties. - `selectId`: A `method` for selecting the primary id for the collection - - `sort`: A compare function used to [sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) the collection. The comparer function is only needed if the collection needs to be sorted before being displayed. Set to `false` to use leave the collection unsorted, which is more performant during CRUD operations. + - `sortComparer`: A compare function used to [sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) the collection. The comparer function is only needed if the collection needs to be sorted before being displayed. Set to `false` to use leave the collection unsorted, which is more performant during CRUD operations. Usage: @@ -31,7 +31,7 @@ export function sortByName(a: User, b: User): number { export const adapter: EntityAdapter = createEntityAdapter({ selectId: (user: User) => user.id, - sort: sortByName, + sortComparer: sortByName, }); ``` diff --git a/example-app/app/books/reducers/books.ts b/example-app/app/books/reducers/books.ts index 589ba961fd..1d8e5ac2bc 100644 --- a/example-app/app/books/reducers/books.ts +++ b/example-app/app/books/reducers/books.ts @@ -20,12 +20,12 @@ export interface State extends EntityState { * functions for single or multiple operations * against the dictionary of records. The configuration * object takes a record id selector function and - * a sort option whether to sort the records when performing - * operations + * a sortComparer option which is set to a compare + * function if the records are to be sorted. */ export const adapter: EntityAdapter = createEntityAdapter({ selectId: (book: Book) => book.id, - sort: false, + sortComparer: false, }); /** getInitialState returns the default initial state diff --git a/modules/entity/spec/sorted_state_adapter.spec.ts b/modules/entity/spec/sorted_state_adapter.spec.ts index f8b8d7ce5d..ca4fe2ef48 100644 --- a/modules/entity/spec/sorted_state_adapter.spec.ts +++ b/modules/entity/spec/sorted_state_adapter.spec.ts @@ -14,7 +14,7 @@ describe('Sorted State Adapter', () => { beforeEach(() => { adapter = createEntityAdapter({ selectId: (book: BookModel) => book.id, - sort: (a, b) => a.title.localeCompare(b.title), + sortComparer: (a, b) => a.title.localeCompare(b.title), }); state = { ids: [], entities: {} }; diff --git a/modules/entity/src/create_adapter.ts b/modules/entity/src/create_adapter.ts index 90467c639c..0526ccf692 100644 --- a/modules/entity/src/create_adapter.ts +++ b/modules/entity/src/create_adapter.ts @@ -12,14 +12,17 @@ import { createUnsortedStateAdapter } from './unsorted_state_adapter'; export function createEntityAdapter(options: { selectId: IdSelector; - sort?: false | Comparer; + sortComparer?: false | Comparer; }): EntityAdapter { - const { selectId, sort }: EntityDefinition = { sort: false, ...options }; + const { selectId, sortComparer }: EntityDefinition = { + sortComparer: false, + ...options, + }; const stateFactory = createInitialStateFactory(); const selectorsFactory = createSelectorsFactory(); - const stateAdapter = sort - ? createSortedStateAdapter(selectId, sort) + const stateAdapter = sortComparer + ? createSortedStateAdapter(selectId, sortComparer) : createUnsortedStateAdapter(selectId); return { diff --git a/modules/entity/src/models.ts b/modules/entity/src/models.ts index a0a4068b64..40f18929de 100644 --- a/modules/entity/src/models.ts +++ b/modules/entity/src/models.ts @@ -22,7 +22,7 @@ export interface EntityState { export interface EntityDefinition { selectId: IdSelector; - sort: false | Comparer; + sortComparer: false | Comparer; } export interface EntityStateAdapter { From 5c60cbae166920f05a66bfedd945549b5de96645 Mon Sep 17 00:00:00 2001 From: ukrukarg Date: Sat, 16 Sep 2017 11:01:14 -0400 Subject: [PATCH 66/67] fix(Store): Refactor parameter initialization in combineReducers for Closure --- modules/store/src/reducer_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/store/src/reducer_manager.ts b/modules/store/src/reducer_manager.ts index 16054875a1..9de8cd061a 100644 --- a/modules/store/src/reducer_manager.ts +++ b/modules/store/src/reducer_manager.ts @@ -40,7 +40,7 @@ export class ReducerManager extends BehaviorSubject> }: StoreFeature) { const reducer = typeof reducers === 'function' - ? (state = initialState, action: any) => reducers(state, action) + ? (state: any, action: any) => reducers(state || initialState, action) : createReducerFactory(reducerFactory, metaReducers)( reducers, initialState From c31573f1c4d9cc02a90cd2033f68d83b17b74969 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 16 Sep 2017 13:49:11 -0500 Subject: [PATCH 67/67] chore(Example): Add Jest as test runner for example tests (#371) --- docs/entity/adapter.md | 2 +- example-app/README.md | 1 + .../login-form.component.spec.ts.snap | 375 ++++++++ .../components/login-form.component.spec.ts | 70 ++ .../login-page.component.spec.ts.snap | 243 +++++ .../containers/login-page.component.spec.ts | 66 ++ .../reducers/__snapshots__/auth.spec.ts.snap | 31 + .../__snapshots__/login-page.spec.ts.snap | 29 + example-app/app/auth/reducers/auth.spec.ts | 19 +- .../app/auth/reducers/login-page.spec.ts | 12 +- .../auth/services/auth-guard.service.spec.ts | 44 + .../app/auth/services/auth-guard.service.ts | 17 +- example-app/app/books/effects/book.spec.ts | 6 +- .../app/books/effects/collection.spec.ts | 24 +- .../reducers/__snapshots__/books.spec.ts.snap | 361 +++++++ example-app/app/books/reducers/books.spec.ts | 15 +- .../app/core/services/google-books.spec.ts | 6 +- example-app/tsconfig.spec.json | 10 +- modules/router-store/spec/integration.spec.ts | 22 +- modules/store/spec/edge.spec.ts | 2 +- modules/store/spec/store.spec.ts | 4 +- package.json | 68 +- setup-jest.ts | 6 + yarn.lock | 896 +++++++++++++++++- 24 files changed, 2214 insertions(+), 115 deletions(-) create mode 100644 example-app/app/auth/components/__snapshots__/login-form.component.spec.ts.snap create mode 100644 example-app/app/auth/components/login-form.component.spec.ts create mode 100644 example-app/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap create mode 100644 example-app/app/auth/containers/login-page.component.spec.ts create mode 100644 example-app/app/auth/reducers/__snapshots__/auth.spec.ts.snap create mode 100644 example-app/app/auth/reducers/__snapshots__/login-page.spec.ts.snap create mode 100644 example-app/app/auth/services/auth-guard.service.spec.ts create mode 100644 example-app/app/books/reducers/__snapshots__/books.spec.ts.snap create mode 100644 setup-jest.ts diff --git a/docs/entity/adapter.md b/docs/entity/adapter.md index 7cb325292a..f2bf430dd3 100644 --- a/docs/entity/adapter.md +++ b/docs/entity/adapter.md @@ -7,7 +7,7 @@ returned adapter provides many [methods](#adapter-methods) for performing operat against the collection type. The method takes an object for configuration with 2 properties. - `selectId`: A `method` for selecting the primary id for the collection - - `sortComparer`: A compare function used to [sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) the collection. The comparer function is only needed if the collection needs to be sorted before being displayed. Set to `false` to use leave the collection unsorted, which is more performant during CRUD operations. + - `sortComparer`: A compare function used to [sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) the collection. The comparer function is only needed if the collection needs to be sorted before being displayed. Set to `false` to leave the collection unsorted, which is more performant during CRUD operations. Usage: diff --git a/example-app/README.md b/example-app/README.md index 78c940d8df..4ed3f3e448 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -18,6 +18,7 @@ Built with [@angular/cli](https://github.com/angular/angular-cli) - [@angular/router](https://github.com/angular/angular) - Angular Router - [@ngrx/db](https://github.com/ngrx/db) - RxJS powered IndexedDB for Angular apps - [@ngrx/store-devtools](https://github.com/ngrx/store-devtools) - Instrumentation for @ngrx/store enabling time-travel debugging + - [jest](https://facebook.github.io/jest/) - JavaScript test runner with easy setup, isolated browser testing and snapshot testing ### Quick start diff --git a/example-app/app/auth/components/__snapshots__/login-form.component.spec.ts.snap b/example-app/app/auth/components/__snapshots__/login-form.component.spec.ts.snap new file mode 100644 index 0000000000..6c03296a87 --- /dev/null +++ b/example-app/app/auth/components/__snapshots__/login-form.component.spec.ts.snap @@ -0,0 +1,375 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Login Page should compile 1`] = ` + + + + + + + Login + + + + + + +
+ + +

+ + + + + + + + + + + +

+ + + +

+ + + + + + + + + + + +

+ + + + + + + +

+ + + + + +

+ + + +
+ + +
+ + +
+ +
+`; + +exports[`Login Page should disable the form if pending 1`] = ` + + + + + + + Login + + + + + + +
+ + +

+ + + + + + + + + + + +

+ + + +

+ + + + + + + + + + + +

+ + + + + + + +

+ + + + + +

+ + + +
+ + +
+ + +
+ +
+`; + +exports[`Login Page should display an error message if provided 1`] = ` + + + + + + + Login + + + + + + +
+ + +

+ + + + + + + + + + + +

+ + + +

+ + + + + + + + + + + +

+ + + + +

+ + Invalid credentials + +

+ + + +

+ + + + + +

+ + + +
+ + +
+ + +
+ +
+`; diff --git a/example-app/app/auth/components/login-form.component.spec.ts b/example-app/app/auth/components/login-form.component.spec.ts new file mode 100644 index 0000000000..99e943cb84 --- /dev/null +++ b/example-app/app/auth/components/login-form.component.spec.ts @@ -0,0 +1,70 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { LoginFormComponent } from './login-form.component'; +import * as Auth from '../actions/auth'; +import * as fromAuth from '../reducers'; +import { ReactiveFormsModule } from '@angular/forms'; + +describe('Login Page', () => { + let fixture: ComponentFixture; + let instance: LoginFormComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [LoginFormComponent], + schemas: [NO_ERRORS_SCHEMA], + }); + + fixture = TestBed.createComponent(LoginFormComponent); + instance = fixture.componentInstance; + }); + + it('should compile', () => { + fixture.detectChanges(); + + /** + * The login form is a presentational component, as it + * only derives its state from inputs and communicates + * externally through outputs. We can use snapshot + * tests to validate the presentation state of this component + * by changing its inputs and snapshotting the generated + * HTML. + * + * We can also use this as a validation tool against changes + * to the component's template against the currently stored + * snapshot. + */ + expect(fixture).toMatchSnapshot(); + }); + + it('should disable the form if pending', () => { + instance.pending = true; + + fixture.detectChanges(); + + expect(fixture).toMatchSnapshot(); + }); + + it('should display an error message if provided', () => { + instance.errorMessage = 'Invalid credentials'; + + fixture.detectChanges(); + + expect(fixture).toMatchSnapshot(); + }); + + it('should emit an event if the form is valid when submitted', () => { + const credentials = { + username: 'user', + password: 'pass', + }; + instance.form.setValue(credentials); + + spyOn(instance.submitted, 'emit'); + instance.submit(); + + expect(instance.submitted.emit).toHaveBeenCalledWith(credentials); + }); +}); diff --git a/example-app/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap b/example-app/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap new file mode 100644 index 0000000000..1b1c954d7e --- /dev/null +++ b/example-app/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Login Page should compile 1`] = ` + + + + + + + + + + Login + + + + + + +
+ + +

+ + + +

+
+ +
+ + + + + + + + + +
+ +
+
+ +
+
+ + +
+ +
+
+
+
+ + + +

+ + + +

+ + + +

+
+ +
+ + + + + + + + + +
+ +
+
+ +
+
+ + +
+ +
+
+
+
+ + + +

+ + + + + + + +

+ + + + + +

+ + + + + + + + + + + + + + + +`; diff --git a/example-app/app/auth/containers/login-page.component.spec.ts b/example-app/app/auth/containers/login-page.component.spec.ts new file mode 100644 index 0000000000..7b52bb2ea3 --- /dev/null +++ b/example-app/app/auth/containers/login-page.component.spec.ts @@ -0,0 +1,66 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { MdInputModule, MdCardModule } from '@angular/material'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { LoginPageComponent } from './login-page.component'; +import { LoginFormComponent } from '../components/login-form.component'; +import * as Auth from '../actions/auth'; +import * as fromAuth from '../reducers'; + +describe('Login Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: LoginPageComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + auth: combineReducers(fromAuth.reducers), + }), + MdInputModule, + MdCardModule, + ReactiveFormsModule, + ], + declarations: [LoginPageComponent, LoginFormComponent], + }); + + fixture = TestBed.createComponent(LoginPageComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + /** + * Container components are used as integration points for connecting + * the store to presentational components and dispatching + * actions to the store. + * + * Container methods that dispatch events are like a component's output observables. + * Container properties that select state from store are like a component's input properties. + * If pure components are functions of their inputs, containers are functions of state + * + * Traditionally you would query the components rendered template + * to validate its state. Since the components are analagous to + * pure functions, we take snapshots of these components for a given state + * to validate the rendered output and verify the component's output + * against changes in state. + */ + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toMatchSnapshot(); + }); + + it('should dispatch a login event on submit', () => { + const $event: any = {}; + const action = new Auth.Login($event); + + instance.onSubmit($event); + + expect(store.dispatch).toHaveBeenCalledWith(action); + }); +}); diff --git a/example-app/app/auth/reducers/__snapshots__/auth.spec.ts.snap b/example-app/app/auth/reducers/__snapshots__/auth.spec.ts.snap new file mode 100644 index 0000000000..c7011251f4 --- /dev/null +++ b/example-app/app/auth/reducers/__snapshots__/auth.spec.ts.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AuthReducer LOGIN_SUCCESS should add a user set loggedIn to true in auth state 1`] = ` +Object { + "loggedIn": true, + "user": Object { + "name": "test", + }, +} +`; + +exports[`AuthReducer LOGOUT should logout a user 1`] = ` +Object { + "loggedIn": false, + "user": null, +} +`; + +exports[`AuthReducer undefined action should return the default state 1`] = ` +Object { + "loggedIn": false, + "user": null, +} +`; + +exports[`AuthReducer wrong login payload should NOT authenticate a user 1`] = ` +Object { + "loggedIn": false, + "user": null, +} +`; diff --git a/example-app/app/auth/reducers/__snapshots__/login-page.spec.ts.snap b/example-app/app/auth/reducers/__snapshots__/login-page.spec.ts.snap new file mode 100644 index 0000000000..ad30237875 --- /dev/null +++ b/example-app/app/auth/reducers/__snapshots__/login-page.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoginPageReducer LOGIN should make pending to true 1`] = ` +Object { + "error": null, + "pending": true, +} +`; + +exports[`LoginPageReducer LOGIN_FAILURE should have an error and no pending state 1`] = ` +Object { + "error": "login failed", + "pending": false, +} +`; + +exports[`LoginPageReducer LOGIN_SUCCESS should have no error and no pending state 1`] = ` +Object { + "error": null, + "pending": false, +} +`; + +exports[`LoginPageReducer undefined action should return the default state 1`] = ` +Object { + "error": null, + "pending": false, +} +`; diff --git a/example-app/app/auth/reducers/auth.spec.ts b/example-app/app/auth/reducers/auth.spec.ts index f41928665c..15a38c3146 100644 --- a/example-app/app/auth/reducers/auth.spec.ts +++ b/example-app/app/auth/reducers/auth.spec.ts @@ -9,7 +9,15 @@ describe('AuthReducer', () => { const action = {} as any; const result = reducer(undefined, action); - expect(result).toEqual(fromAuth.initialState); + + /** + * Snapshot tests are a quick way to validate + * the state produced by a reducer since + * its plain JavaScript object. These snapshots + * are used to validate against the current state + * if the functionality of the reducer ever changes. + */ + expect(result).toMatchSnapshot(); }); }); @@ -21,7 +29,8 @@ describe('AuthReducer', () => { const expectedResult = fromAuth.initialState; const result = reducer(fromAuth.initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); @@ -36,7 +45,8 @@ describe('AuthReducer', () => { }; const result = reducer(fromAuth.initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); @@ -51,7 +61,8 @@ describe('AuthReducer', () => { const expectedResult = fromAuth.initialState; const result = reducer(initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); }); diff --git a/example-app/app/auth/reducers/login-page.spec.ts b/example-app/app/auth/reducers/login-page.spec.ts index c25f222a5e..115a1b639c 100644 --- a/example-app/app/auth/reducers/login-page.spec.ts +++ b/example-app/app/auth/reducers/login-page.spec.ts @@ -9,7 +9,8 @@ describe('LoginPageReducer', () => { const action = {} as any; const result = reducer(undefined, action); - expect(result).toEqual(fromLoginPage.initialState); + + expect(result).toMatchSnapshot(); }); }); @@ -24,7 +25,8 @@ describe('LoginPageReducer', () => { }; const result = reducer(fromLoginPage.initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); @@ -39,7 +41,8 @@ describe('LoginPageReducer', () => { }; const result = reducer(fromLoginPage.initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); @@ -54,7 +57,8 @@ describe('LoginPageReducer', () => { }; const result = reducer(fromLoginPage.initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); }); }); }); diff --git a/example-app/app/auth/services/auth-guard.service.spec.ts b/example-app/app/auth/services/auth-guard.service.spec.ts new file mode 100644 index 0000000000..dcc3a41b8b --- /dev/null +++ b/example-app/app/auth/services/auth-guard.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { cold } from 'jasmine-marbles'; +import { AuthGuard } from './auth-guard.service'; +import * as Auth from '../actions/auth'; +import * as fromRoot from '../../reducers'; +import * as fromAuth from '../reducers'; + +describe('Auth Guard', () => { + let guard: AuthGuard; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ + ...fromRoot.reducers, + auth: combineReducers(fromAuth.reducers), + }), + ], + providers: [AuthGuard], + }); + + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + guard = TestBed.get(AuthGuard); + }); + + it('should return false if the user state is not logged in', () => { + const expected = cold('(a|)', { a: false }); + + expect(guard.canActivate()).toBeObservable(expected); + }); + + it('should return true if the user state is logged in', () => { + const user: any = {}; + const action = new Auth.LoginSuccess({ user }); + store.dispatch(action); + + const expected = cold('(a|)', { a: true }); + + expect(guard.canActivate()).toBeObservable(expected); + }); +}); diff --git a/example-app/app/auth/services/auth-guard.service.ts b/example-app/app/auth/services/auth-guard.service.ts index 875e8174f3..f2e5b2414c 100644 --- a/example-app/app/auth/services/auth-guard.service.ts +++ b/example-app/app/auth/services/auth-guard.service.ts @@ -12,13 +12,16 @@ export class AuthGuard implements CanActivate { constructor(private store: Store) {} canActivate(): Observable { - return this.store.select(fromAuth.getLoggedIn).take(1).map(authed => { - if (!authed) { - this.store.dispatch(new Auth.LoginRedirect()); - return false; - } + return this.store + .select(fromAuth.getLoggedIn) + .map(authed => { + if (!authed) { + this.store.dispatch(new Auth.LoginRedirect()); + return false; + } - return true; - }); + return true; + }) + .take(1); } } diff --git a/example-app/app/books/effects/book.spec.ts b/example-app/app/books/effects/book.spec.ts index dbe58a4bf5..c2b7e07c40 100644 --- a/example-app/app/books/effects/book.spec.ts +++ b/example-app/app/books/effects/book.spec.ts @@ -33,7 +33,7 @@ describe('BookEffects', () => { BookEffects, { provide: GoogleBooksService, - useValue: jasmine.createSpyObj('GoogleBooksService', ['searchBooks']), + useValue: { searchBooks: jest.fn() }, }, { provide: Actions, useFactory: getActions }, { provide: SEARCH_SCHEDULER, useFactory: getTestScheduler }, @@ -57,7 +57,7 @@ describe('BookEffects', () => { actions$.stream = hot('-a---', { a: action }); const response = cold('-a|', { a: books }); const expected = cold('-----b', { b: completion }); - googleBooksService.searchBooks.and.returnValue(response); + googleBooksService.searchBooks = jest.fn(() => response); expect(effects.search$).toBeObservable(expected); }); @@ -70,7 +70,7 @@ describe('BookEffects', () => { actions$.stream = hot('-a---', { a: action }); const response = cold('-#|', {}, error); const expected = cold('-----b', { b: completion }); - googleBooksService.searchBooks.and.returnValue(response); + googleBooksService.searchBooks = jest.fn(() => response); expect(effects.search$).toBeObservable(expected); }); diff --git a/example-app/app/books/effects/collection.spec.ts b/example-app/app/books/effects/collection.spec.ts index c2a4eb8774..e2032d4a8c 100644 --- a/example-app/app/books/effects/collection.spec.ts +++ b/example-app/app/books/effects/collection.spec.ts @@ -36,12 +36,12 @@ describe('CollectionEffects', () => { CollectionEffects, { provide: Database, - useValue: jasmine.createSpyObj('database', [ - 'open', - 'query', - 'insert', - 'executeWrite', - ]), + useValue: { + open: jest.fn(), + query: jest.fn(), + insert: jest.fn(), + executeWrite: jest.fn(), + }, }, { provide: Actions, useFactory: getActions }, ], @@ -67,7 +67,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-a-b|', { a: book1, b: book2 }); const expected = cold('-----c', { c: completion }); - db.query.and.returnValue(response); + db.query = jest.fn(() => response); expect(effects.loadCollection$).toBeObservable(expected); }); @@ -80,7 +80,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-#', {}, error); const expected = cold('--c', { c: completion }); - db.query.and.returnValue(response); + db.query = jest.fn(() => response); expect(effects.loadCollection$).toBeObservable(expected); }); @@ -94,7 +94,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-b', { b: true }); const expected = cold('--c', { c: completion }); - db.insert.and.returnValue(response); + db.insert = jest.fn(() => response); expect(effects.addBookToCollection$).toBeObservable(expected); expect(db.insert).toHaveBeenCalledWith('books', [book1]); @@ -108,7 +108,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-#', {}, error); const expected = cold('--c', { c: completion }); - db.insert.and.returnValue(response); + db.insert = jest.fn(() => response); expect(effects.addBookToCollection$).toBeObservable(expected); }); @@ -121,7 +121,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-b', { b: true }); const expected = cold('--c', { c: completion }); - db.executeWrite.and.returnValue(response); + db.executeWrite = jest.fn(() => response); expect(effects.removeBookFromCollection$).toBeObservable(expected); expect(db.executeWrite).toHaveBeenCalledWith('books', 'delete', [ @@ -137,7 +137,7 @@ describe('CollectionEffects', () => { actions$.stream = hot('-a', { a: action }); const response = cold('-#', {}, error); const expected = cold('--c', { c: completion }); - db.executeWrite.and.returnValue(response); + db.executeWrite = jest.fn(() => response); expect(effects.removeBookFromCollection$).toBeObservable(expected); expect(db.executeWrite).toHaveBeenCalledWith('books', 'delete', [ diff --git a/example-app/app/books/reducers/__snapshots__/books.spec.ts.snap b/example-app/app/books/reducers/__snapshots__/books.spec.ts.snap new file mode 100644 index 0000000000..3ebd4bd978 --- /dev/null +++ b/example-app/app/books/reducers/__snapshots__/books.spec.ts.snap @@ -0,0 +1,361 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BooksReducer LOAD should add a single book, if the book does not exist 1`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer LOAD should return the existing state if the book exists 1`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add all books in the payload when none exist 1`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "222": Object { + "id": "222", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + "222", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add all books in the payload when none exist 2`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "222": Object { + "id": "222", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + "222", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only new books when books already exist 1`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "222": Object { + "id": "222", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "333": Object { + "id": "333", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + "222", + "333", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only new books when books already exist 2`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "222": Object { + "id": "222", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "333": Object { + "id": "333", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + "222", + "333", + ], + "selectedBookId": null, +} +`; + +exports[`BooksReducer SELECT should set the selected book id on the state 1`] = ` +Object { + "entities": Object { + "1": Object { + "id": "1", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + "222": Object { + "id": "222", + "volumeInfo": Object { + "authors": Array [ + "author", + ], + "averageRating": 3, + "description": "description", + "imageLinks": Object { + "smallThumbnail": "string", + "thumbnail": "string", + }, + "publishDate": "", + "publisher": "publisher", + "ratingsCount": 5, + "subtitle": "subtitle", + "title": "title", + }, + }, + }, + "ids": Array [ + "1", + "222", + ], + "selectedBookId": "1", +} +`; + +exports[`BooksReducer Selectors getSelectedId should return the selected id 1`] = `"1"`; + +exports[`BooksReducer undefined action should return the default state 1`] = ` +Object { + "entities": Object {}, + "ids": Array [], + "selectedBookId": null, +} +`; diff --git a/example-app/app/books/reducers/books.spec.ts b/example-app/app/books/reducers/books.spec.ts index 47560daa80..8b78b2fb39 100644 --- a/example-app/app/books/reducers/books.spec.ts +++ b/example-app/app/books/reducers/books.spec.ts @@ -21,7 +21,7 @@ describe('BooksReducer', () => { it('should return the default state', () => { const result = reducer(undefined, {} as any); - expect(result).toEqual(fromBooks.initialState); + expect(result).toMatchSnapshot(); }); }); @@ -36,7 +36,7 @@ describe('BooksReducer', () => { const result = reducer(booksInitialState, createAction); - expect(result).toEqual(initialState); + expect(result).toMatchSnapshot(); } function existingBooks(action: any, initialState: any, books: Book[]) { @@ -54,7 +54,8 @@ describe('BooksReducer', () => { }; const result = reducer(initialState, createAction); - expect(result).toEqual(expectedResult); + + expect(result).toMatchSnapshot(); } it('should add all books in the payload when none exist', () => { @@ -90,7 +91,7 @@ describe('BooksReducer', () => { const result = reducer(fromBooks.initialState, action); - expect(result).toEqual(expectedResult); + expect(result).toMatchSnapshot(); }); it('should return the existing state if the book exists', () => { @@ -98,7 +99,7 @@ describe('BooksReducer', () => { const result = reducer(expectedResult, action); - expect(result).toEqual(expectedResult); + expect(result).toMatchSnapshot(); }); }); @@ -108,7 +109,7 @@ describe('BooksReducer', () => { const result = reducer(initialState, action); - expect(result.selectedBookId).toBe(book1.id); + expect(result).toMatchSnapshot(); }); }); @@ -120,7 +121,7 @@ describe('BooksReducer', () => { selectedBookId: book1.id, }); - expect(result).toBe(book1.id); + expect(result).toMatchSnapshot(); }); }); }); diff --git a/example-app/app/core/services/google-books.spec.ts b/example-app/app/core/services/google-books.spec.ts index 30dd60160d..d525ef1e0a 100644 --- a/example-app/app/core/services/google-books.spec.ts +++ b/example-app/app/core/services/google-books.spec.ts @@ -10,7 +10,7 @@ describe('Service: GoogleBooks', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - { provide: Http, useValue: jasmine.createSpyObj('Http', ['get']) }, + { provide: Http, useValue: { get: jest.fn() } }, GoogleBooksService, ], }); @@ -41,7 +41,7 @@ describe('Service: GoogleBooks', () => { const response = cold('-a|', { a: httpResponse }); const expected = cold('-b|', { b: books.items }); - http.get.and.returnValue(response); + http.get = jest.fn(() => response); expect(service.searchBooks(queryTitle)).toBeObservable(expected); expect(http.get).toHaveBeenCalledWith( @@ -56,7 +56,7 @@ describe('Service: GoogleBooks', () => { const response = cold('-a|', { a: httpResponse }); const expected = cold('-b|', { b: data }); - http.get.and.returnValue(response); + http.get = jest.fn(() => response); expect(service.retrieveBook(data.volumeId)).toBeObservable(expected); expect(http.get).toHaveBeenCalledWith( diff --git a/example-app/tsconfig.spec.json b/example-app/tsconfig.spec.json index 19a8a96901..adf08a6c74 100644 --- a/example-app/tsconfig.spec.json +++ b/example-app/tsconfig.spec.json @@ -17,15 +17,7 @@ "node" ], "baseUrl": ".", - "rootDir": "../", - "paths": { - "@ngrx/effects": ["../modules/effects"], - "@ngrx/effects/testing": ["../modules/effects/testing"], - "@ngrx/store": ["../modules/store"], - "@ngrx/router-store": ["../modules/router-store"], - "@ngrx/store-devtools": ["../modules/store-devtools"], - "@ngrx/entity": ["../modules/entity"] - } + "rootDir": "../" }, "files": [ "test.ts" diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index 9e0519a881..f164bf87a1 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -20,7 +20,7 @@ import 'rxjs/add/operator/toPromise'; import { of } from 'rxjs/observable/of'; describe('integration spec', () => { - it('should work', done => { + it('should work', (done: any) => { const reducer = (state: string = '', action: RouterAction) => { if (action.type === ROUTER_NAVIGATION) { return action.payload.routerState.url.toString(); @@ -62,7 +62,7 @@ describe('integration spec', () => { }); }); - it('should support preventing navigation', done => { + it('should support preventing navigation', (done: any) => { const reducer = (state: string = '', action: RouterAction) => { if ( action.type === ROUTER_NAVIGATION && @@ -98,7 +98,7 @@ describe('integration spec', () => { }); }); - it('should support rolling back if navigation gets canceled', done => { + it('should support rolling back if navigation gets canceled', (done: any) => { const reducer = (state: string = '', action: RouterAction): any => { if (action.type === ROUTER_NAVIGATION) { return { @@ -156,7 +156,7 @@ describe('integration spec', () => { }); }); - it('should support rolling back if navigation errors', done => { + it('should support rolling back if navigation errors', (done: any) => { const reducer = (state: string = '', action: RouterAction): any => { if (action.type === ROUTER_NAVIGATION) { return { @@ -216,7 +216,9 @@ describe('integration spec', () => { }); }); - it('should call navigateByUrl when resetting state of the routerReducer', done => { + it('should call navigateByUrl when resetting state of the routerReducer', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state @@ -291,7 +293,9 @@ describe('integration spec', () => { }); }); - it('should support cancellation of initial navigation using canLoad guard', done => { + it('should support cancellation of initial navigation using canLoad guard', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state @@ -320,7 +324,9 @@ describe('integration spec', () => { done(); }); - it('should support a custom RouterStateSnapshot serializer ', done => { + it('should support a custom RouterStateSnapshot serializer ', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state @@ -374,7 +380,7 @@ describe('integration spec', () => { }); }); - it('should support event during an async canActivate guard', done => { + it('should support event during an async canActivate guard', (done: any) => { createTestModule({ reducers: { routerReducer }, canActivate: () => { diff --git a/modules/store/spec/edge.spec.ts b/modules/store/spec/edge.spec.ts index 0c5016dcda..00d9a795f7 100644 --- a/modules/store/spec/edge.spec.ts +++ b/modules/store/spec/edge.spec.ts @@ -32,7 +32,7 @@ describe('ngRx Store', () => { expect(store).toBeDefined(); }); - it('should handle re-entrancy', done => { + it('should handle re-entrancy', (done: any) => { let todosNextCount = 0; let todosCountNextCount = 0; diff --git a/modules/store/spec/store.spec.ts b/modules/store/spec/store.spec.ts index 3fe0f11dae..de310a6aed 100644 --- a/modules/store/spec/store.spec.ts +++ b/modules/store/spec/store.spec.ts @@ -37,7 +37,7 @@ describe('ngRx Store', () => { } describe('initial state', () => { - it('should handle an initial state object', done => { + it('should handle an initial state object', (done: any) => { setup(); store.take(1).subscribe({ @@ -49,7 +49,7 @@ describe('ngRx Store', () => { }); }); - it('should handle an initial state function', done => { + it('should handle an initial state function', (done: any) => { setup(() => ({ counter1: 0, counter2: 5 })); store.take(1).subscribe({ diff --git a/package.json b/package.json index 04f8be9bc6..0e3040040a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "coverage:html": "nyc report --reporter=html", "example:start": "yarn run build && yarn run cli -- serve", "example:start:aot": "yarn run build && yarn run cli -- serve --aot", - "example:test": "yarn run cli -- test --code-coverage", + "example:test": "jest --watch", "example:build:prod": "yarn build && yarn cli -- build --aot -prod --base-href \"/platform/example-app/\" --output-path \"./example-dist/example-app\"", "ci": "yarn run build && yarn run test && nyc report --reporter=text-lcov | coveralls", "prettier": "prettier --parser typescript --single-quote --trailing-comma es5 --write \"./**/*.ts\"", @@ -24,16 +24,30 @@ "release": "lerna publish --skip-npm --conventional-commits && npm run build" }, "lint-staged": { - "*.ts": ["yarn prettier", "git add"] + "*.ts": [ + "yarn prettier", + "git add" + ] }, - "keywords": ["ngrx", "angular", "rxjs"], + "keywords": [ + "ngrx", + "angular", + "rxjs" + ], "author": "Rob Wormald ", "license": "MIT", "repository": {}, "nyc": { - "extension": [".ts"], - "exclude": ["**/*.spec", "**/spec/**/*"], - "include": ["**/*.ts"] + "extension": [ + ".ts" + ], + "exclude": [ + "**/*.spec", + "**/spec/**/*" + ], + "include": [ + "**/*.ts" + ] }, "devDependencies": { "@angular/animations": "^4.2.0", @@ -52,7 +66,9 @@ "@ngrx/db": "^2.0.1", "@types/fs-extra": "^2.1.0", "@types/glob": "^5.0.30", - "@types/jasmine": "2.5.38", + "@types/jasmine": "2.5.45", + "@types/jasminewd2": "^2.0.2", + "@types/jest": "^20.0.2", "@types/node": "^7.0.5", "@types/ora": "^0.3.31", "@types/rimraf": "^0.0.28", @@ -72,6 +88,9 @@ "jasmine-core": "~2.5.2", "jasmine-marbles": "^0.0.2", "jasmine-spec-reporter": "~3.2.0", + "jest": "^21.0.2", + "jest-preset-angular": "^3.0.1", + "jest-zone-patch": "^0.0.7", "karma": "~1.4.1", "karma-chrome-launcher": "~2.0.0", "karma-cli": "~1.0.1", @@ -106,5 +125,40 @@ "type": "opencollective", "url": "https://opencollective.com/ngrx", "logo": "https://opencollective.com/opencollective/logo.txt" + }, + "jest": { + "setupTestFrameworkScriptFile": "/setup-jest.ts", + "globals": { + "ts-jest": { + "tsConfigFile": "example-app/tsconfig.spec.json" + }, + "__TRANSFORM_HTML__": true + }, + "transform": { + "^.+\\.(ts|js|html)$": "/node_modules/jest-preset-angular/preprocessor.js" + }, + "testMatch": [ + "/example-app/**/*.spec.ts" + ], + "moduleFileExtensions": [ + "ts", + "js", + "html", + "json" + ], + "mapCoverage": true, + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/modules/*.*/" + ], + "moduleNameMapper": { + "^@ngrx/(?!db)(.*)": "/modules/$1" + }, + "transformIgnorePatterns": [ + "node_modules/(?!@ngrx)" + ], + "modulePathIgnorePatterns": [ + "dist" + ] } } diff --git a/setup-jest.ts b/setup-jest.ts new file mode 100644 index 0000000000..05a89c72cc --- /dev/null +++ b/setup-jest.ts @@ -0,0 +1,6 @@ +import 'jest-preset-angular'; +import { MdCommonModule } from '@angular/material'; + +global['CSS'] = null; +MdCommonModule.prototype['_checkDoctype'] = function() {}; +MdCommonModule.prototype['_checkTheme'] = function() {}; diff --git a/yarn.lock b/yarn.lock index 64eefd0870..cce51a858d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,9 +186,19 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/jasmine@2.5.38": - version "2.5.38" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.5.38.tgz#a4379124c4921d4e21de54ec74669c9e9b356717" +"@types/jasmine@*", "@types/jasmine@2.5.45": + version "2.5.45" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.5.45.tgz#58928a621d014ce6ab59c5a9c41071f7328b0ca9" + +"@types/jasminewd2@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.3.tgz#0d2886b0cbdae4c0eeba55e30792f584bf040a95" + dependencies: + "@types/jasmine" "*" + +"@types/jest@^20.0.2": + version "20.0.8" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-20.0.8.tgz#7f8c97f73d20d3bf5448fbe33661a342002b5954" "@types/minimatch@*": version "2.0.29" @@ -227,6 +237,10 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" +abab@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" + abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" @@ -244,7 +258,13 @@ acorn-dynamic-import@^2.0.0: dependencies: acorn "^4.0.3" -acorn@^4.0.3: +acorn-globals@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" + dependencies: + acorn "^4.0.4" + +acorn@^4.0.3, acorn@^4.0.4: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" @@ -321,6 +341,10 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -329,6 +353,10 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -339,6 +367,12 @@ ansi-styles@^3.1.0: dependencies: color-convert "^1.0.0" +ansi-styles@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + anymatch@^1.1.0, anymatch@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" @@ -387,6 +421,10 @@ arr-flatten@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" @@ -467,6 +505,10 @@ assert@^1.1.1: dependencies: util "0.10.3" +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -516,7 +558,7 @@ aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -babel-code-frame@^6.11.0, babel-code-frame@^6.20.0, babel-code-frame@^6.22.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.20.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -524,6 +566,38 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.20.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.0.0, babel-core@^6.24.1, babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + babel-generator@^6.18.0: version "6.24.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.0.tgz#eba270a8cc4ce6e09a61be43465d7c62c1f87c56" @@ -537,12 +611,67 @@ babel-generator@^6.18.0: source-map "^0.5.0" trim-right "^1.0.1" +babel-generator@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.6" + trim-right "^1.0.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-jest@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.0.2.tgz#817ea52c23f1c6c4b684d6960968416b6a9e9c6c" + dependencies: + babel-plugin-istanbul "^4.0.0" + babel-preset-jest "^21.0.2" + babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" dependencies: babel-runtime "^6.22.0" +babel-plugin-istanbul@^4.0.0, babel-plugin-istanbul@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.4.tgz#18dde84bf3ce329fddf3f4103fae921456d8e587" + dependencies: + find-up "^2.1.0" + istanbul-lib-instrument "^1.7.2" + test-exclude "^4.1.1" + +babel-plugin-jest-hoist@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.0.2.tgz#cfdce5bca40d772a056cb8528ad159c7bb4bb03d" + +babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + babel-polyfill@6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" @@ -551,6 +680,24 @@ babel-polyfill@6.23.0: core-js "^2.4.0" regenerator-runtime "^0.10.0" +babel-preset-jest@^21.0.0, babel-preset-jest@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.0.2.tgz#9db25def2329f49eace3f5ea0de42a0b898d12cc" + dependencies: + babel-plugin-jest-hoist "^21.0.2" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + babel-runtime@^6.18.0, babel-runtime@^6.22.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" @@ -558,6 +705,13 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0: core-js "^2.4.0" regenerator-runtime "^0.10.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + babel-template@^6.16.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638" @@ -568,7 +722,17 @@ babel-template@^6.16.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.18.0, babel-traverse@^6.23.0: +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.18.0: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" dependencies: @@ -582,7 +746,21 @@ babel-traverse@^6.18.0, babel-traverse@^6.23.0: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.18.0, babel-types@^6.23.0: +babel-traverse@^6.23.0, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.18.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" dependencies: @@ -591,9 +769,18 @@ babel-types@^6.18.0, babel-types@^6.23.0: lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.15.0, babylon@^6.17.4: - version "6.17.4" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" +babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.11.0, babylon@^6.15.0, babylon@^6.17.4, babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" babylon@^6.13.0: version "6.16.1" @@ -737,6 +924,12 @@ brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" +browser-resolve@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" @@ -795,6 +988,12 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + buffer-crc32@^0.2.5: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -847,6 +1046,10 @@ callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + camel-case@3.0.x: version "3.0.0" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" @@ -1076,7 +1279,7 @@ codelyzer@^2.1.1: source-map "^0.5.6" sprintf-js "^1.0.3" -color-convert@^1.0.0, color-convert@^1.3.0: +color-convert@^1.0.0, color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1246,6 +1449,10 @@ content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" +content-type-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + content-type@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" @@ -1396,10 +1603,14 @@ conventional-recommended-bump@^1.0.0: meow "^3.3.0" object-assign "^4.0.1" -convert-source-map@^1.3.0: +convert-source-map@^1.3.0, convert-source-map@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3" +convert-source-map@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1412,6 +1623,10 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" +core-js@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" + core-object@^3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/core-object/-/core-object-3.1.3.tgz#df399b3311bdb0c909e8aae8929fc3c1c4b25880" @@ -1669,6 +1884,16 @@ csso@~2.3.1: clap "^1.0.9" source-map "^0.5.3" +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" + +"cssstyle@>= 0.2.37 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + dependencies: + cssom "0.3.x" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1760,6 +1985,10 @@ deep-freeze@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -1833,7 +2062,7 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@^3.0.1, diff@^3.1.0: +diff@^3.0.1, diff@^3.1.0, diff@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" @@ -2040,7 +2269,7 @@ entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -errno@^0.1.1, errno@^0.1.3: +errno@^0.1.1, errno@^0.1.3, errno@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" dependencies: @@ -2064,14 +2293,33 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" +escodegen@^1.6.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.5.6" + esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" +estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2100,6 +2348,12 @@ evp_bytestokey@^1.0.0: dependencies: create-hash "^1.1.1" +exec-sh@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" + dependencies: + merge "^1.1.3" + execa@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36" @@ -2183,6 +2437,17 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +expect@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-21.0.2.tgz#b34abf0635ec9d6aea1ce7edb4722afe86c4a38f" + dependencies: + ansi-styles "^3.2.0" + jest-diff "^21.0.2" + jest-get-type "^21.0.2" + jest-matcher-utils "^21.0.2" + jest-message-util "^21.0.2" + jest-regex-util "^21.0.2" + exports-loader@^0.6.3: version "0.6.4" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886" @@ -2256,6 +2521,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -2272,6 +2541,12 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + figures@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2432,11 +2707,19 @@ fs-extra@^3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" +fs-extra@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0: +fsevents@^1.0.0, fsevents@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" dependencies: @@ -2625,6 +2908,10 @@ globals@^9.0.0: version "9.17.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + globby@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-4.1.0.tgz#080f54549ec1b82a6c60e631fc82e1211dbe95f8" @@ -2689,6 +2976,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + hammerjs@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1" @@ -2820,6 +3111,13 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + hosted-git-info@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5" @@ -2837,6 +3135,12 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" +html-encoding-sniffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + dependencies: + whatwg-encoding "^1.0.1" + html-entities@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" @@ -2931,6 +3235,10 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" +iconv-lite@0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + iconv-lite@0.4.15, iconv-lite@~0.4.13: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" @@ -3034,7 +3342,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.2.0: +invariant@^2.2.0, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3257,10 +3565,6 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -3332,7 +3636,7 @@ istanbul-lib-instrument@^1.1.3, istanbul-lib-instrument@^1.4.2: istanbul-lib-coverage "^1.0.0" semver "^5.3.0" -istanbul-lib-instrument@^1.7.3: +istanbul-lib-instrument@^1.7.2, istanbul-lib-instrument@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.3.tgz#925b239163eabdd68cc4048f52c2fa4f899ecfa7" dependencies: @@ -3427,6 +3731,242 @@ jasminewd2@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.1.0.tgz#da595275d1ae631de736ac0a7c7d85c9f73ef652" +jest-changed-files@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.0.2.tgz#0a74f35cf2d3b7c8ef9ab4fac0a75409f81ec1b0" + dependencies: + throat "^4.0.0" + +jest-cli@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.0.2.tgz#2e08af63d44fc21284ebf496cf71e381f3cc9786" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + is-ci "^1.0.10" + istanbul-api "^1.1.1" + istanbul-lib-coverage "^1.0.1" + istanbul-lib-instrument "^1.4.2" + istanbul-lib-source-maps "^1.1.0" + jest-changed-files "^21.0.2" + jest-config "^21.0.2" + jest-environment-jsdom "^21.0.2" + jest-haste-map "^21.0.2" + jest-message-util "^21.0.2" + jest-regex-util "^21.0.2" + jest-resolve-dependencies "^21.0.2" + jest-runner "^21.0.2" + jest-runtime "^21.0.2" + jest-snapshot "^21.0.2" + jest-util "^21.0.2" + micromatch "^2.3.11" + node-notifier "^5.0.2" + pify "^3.0.0" + slash "^1.0.0" + string-length "^2.0.0" + strip-ansi "^4.0.0" + which "^1.2.12" + worker-farm "^1.3.1" + yargs "^9.0.0" + +jest-config@^21.0.0, jest-config@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.0.2.tgz#ea42b94f3c22ae4e4aa11c69f5b45e34e342199d" + dependencies: + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^21.0.2" + jest-environment-node "^21.0.2" + jest-get-type "^21.0.2" + jest-jasmine2 "^21.0.2" + jest-regex-util "^21.0.2" + jest-resolve "^21.0.2" + jest-util "^21.0.2" + jest-validate "^21.0.2" + pretty-format "^21.0.2" + +jest-diff@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.0.2.tgz#751014f36ad5d505f6affce5542fde0e444ee50a" + dependencies: + chalk "^2.0.1" + diff "^3.2.0" + jest-get-type "^21.0.2" + pretty-format "^21.0.2" + +jest-docblock@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.0.2.tgz#66f69ddb440799fc32f91d0ac3d8d35e99e2032f" + +jest-environment-jsdom@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.0.2.tgz#6f6ab5bd71970d1900fbd47a46701c0a07fa3be5" + dependencies: + jest-mock "^21.0.2" + jest-util "^21.0.2" + jsdom "^9.12.0" + +jest-environment-node@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.0.2.tgz#4267ceb39551f8ecaed182ab882a93ef4d5de240" + dependencies: + jest-mock "^21.0.2" + jest-util "^21.0.2" + +jest-get-type@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.0.2.tgz#304e6b816dd33cd1f47aba0597bcad258a509fc6" + +jest-haste-map@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.0.2.tgz#bd98bc6cd6f207eb029b2f5918da1a9347eb11b7" + dependencies: + fb-watchman "^2.0.0" + graceful-fs "^4.1.11" + jest-docblock "^21.0.2" + micromatch "^2.3.11" + sane "^2.0.0" + worker-farm "^1.3.1" + +jest-jasmine2@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.0.2.tgz#a368abb3a686def4d6e763509a265104943cd469" + dependencies: + chalk "^2.0.1" + expect "^21.0.2" + graceful-fs "^4.1.11" + jest-diff "^21.0.2" + jest-matcher-utils "^21.0.2" + jest-message-util "^21.0.2" + jest-snapshot "^21.0.2" + p-cancelable "^0.3.0" + +jest-matcher-utils@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.0.2.tgz#eb6736a45b698546d71f7e1ffbbd36587eeb27bc" + dependencies: + chalk "^2.0.1" + jest-get-type "^21.0.2" + pretty-format "^21.0.2" + +jest-message-util@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.0.2.tgz#81242e07d426ad54c15f3d7c55b072e9db7b39a9" + dependencies: + chalk "^2.0.1" + micromatch "^2.3.11" + slash "^1.0.0" + +jest-mock@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.0.2.tgz#5e92902450e1ce78be3864cc4d50dbd5d1582fbd" + +jest-preset-angular@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-3.0.1.tgz#b59234234ced305d777c4b5bd98433a93f38cee8" + dependencies: + jest-zone-patch "^0.0.7" + ts-jest "^21.0.0" + +jest-regex-util@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.0.2.tgz#06248c07b53ff444223ebe8e33a25bc051ac976f" + +jest-resolve-dependencies@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.0.2.tgz#c42cc371034023ac1a226a7a52f86233c8871938" + dependencies: + jest-regex-util "^21.0.2" + +jest-resolve@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.0.2.tgz#57b2c20cbeca2357eb5e638d5c28beca7f38c3f8" + dependencies: + browser-resolve "^1.11.2" + chalk "^2.0.1" + is-builtin-module "^1.0.0" + +jest-runner@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.0.2.tgz#1462d431d25f7744e8b5e03837bbf9e268dc8b15" + dependencies: + jest-config "^21.0.2" + jest-docblock "^21.0.2" + jest-haste-map "^21.0.2" + jest-jasmine2 "^21.0.2" + jest-message-util "^21.0.2" + jest-runtime "^21.0.2" + jest-util "^21.0.2" + pify "^3.0.0" + throat "^4.0.0" + worker-farm "^1.3.1" + +jest-runtime@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.0.2.tgz#ce26ba06bcd5501991bd994b1eacc0c7c7913895" + dependencies: + babel-core "^6.0.0" + babel-jest "^21.0.2" + babel-plugin-istanbul "^4.0.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + graceful-fs "^4.1.11" + jest-config "^21.0.2" + jest-haste-map "^21.0.2" + jest-regex-util "^21.0.2" + jest-resolve "^21.0.2" + jest-util "^21.0.2" + json-stable-stringify "^1.0.1" + micromatch "^2.3.11" + slash "^1.0.0" + strip-bom "3.0.0" + write-file-atomic "^2.1.0" + yargs "^9.0.0" + +jest-snapshot@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.0.2.tgz#5b8f4dd05c1759381db835451fba4bcd85a55611" + dependencies: + chalk "^2.0.1" + jest-diff "^21.0.2" + jest-matcher-utils "^21.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^21.0.2" + +jest-util@^21.0.0, jest-util@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.0.2.tgz#3ee2380af25c414a39f07b23c84da6f2d5f1f76a" + dependencies: + callsites "^2.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.11" + jest-message-util "^21.0.2" + jest-mock "^21.0.2" + jest-validate "^21.0.2" + mkdirp "^0.5.1" + +jest-validate@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.0.2.tgz#dd066b257bd102759c214747d73bed6bcfa4349d" + dependencies: + chalk "^2.0.1" + jest-get-type "^21.0.2" + leven "^2.1.0" + pretty-format "^21.0.2" + +jest-zone-patch@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/jest-zone-patch/-/jest-zone-patch-0.0.7.tgz#d963cd3eb005d500db1b41242a7a63a371a500f3" + +jest@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-21.0.2.tgz#a5c9bdc9d4322ae672fe8cb3eaf25c268c5f04b2" + dependencies: + jest-cli "^21.0.2" + jodid25519@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" @@ -3441,6 +3981,10 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + js-yaml@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" @@ -3466,6 +4010,30 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +jsdom@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" + dependencies: + abab "^1.0.3" + acorn "^4.0.4" + acorn-globals "^3.1.0" + array-equal "^1.0.0" + content-type-parser "^1.0.1" + cssom ">= 0.3.2 < 0.4.0" + cssstyle ">= 0.2.37 < 0.3.0" + escodegen "^1.6.1" + html-encoding-sniffer "^1.0.1" + nwmatcher ">= 1.3.9 < 2.0.0" + parse5 "^1.5.1" + request "^2.79.0" + sax "^1.2.1" + symbol-tree "^3.2.1" + tough-cookie "^2.3.2" + webidl-conversions "^4.0.0" + whatwg-encoding "^1.0.1" + whatwg-url "^4.3.0" + xml-name-validator "^2.0.1" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -3701,6 +4269,17 @@ less@^2.7.2: request "^2.72.0" source-map "^0.5.3" +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + license-webpack-plugin@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-0.4.3.tgz#f9d88d4ebc04407a0061e8ccac26571f88e51a16" @@ -3956,6 +4535,12 @@ make-error@^1.1.1: version "1.2.3" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.2.3.tgz#6c4402df732e0977ac6faf754a5074b3d2b1d19d" +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -4022,6 +4607,10 @@ merge-source-map@^1.0.2: dependencies: source-map "^0.5.3" +merge@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -4091,7 +4680,7 @@ minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -4144,6 +4733,10 @@ nan@^2.3.0, nan@^2.3.2: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + ncname@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c" @@ -4197,6 +4790,10 @@ node-gyp@^3.3.1: tar "^2.0.0" which "1" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + node-libs-browser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" @@ -4229,6 +4826,15 @@ node-modules-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/node-modules-path/-/node-modules-path-1.0.1.tgz#40096b08ce7ad0ea14680863af449c7c75a5d1c8" +node-notifier@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" + dependencies: + growly "^1.3.0" + semver "^5.3.0" + shellwords "^0.1.0" + which "^1.2.12" + node-pre-gyp@^0.6.36: version "0.6.36" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" @@ -4356,6 +4962,10 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" +"nwmatcher@>= 1.3.9 < 2.0.0": + version "1.4.1" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.1.tgz#7ae9b07b0ea804db7e25f05cb5fe4097d4e4949f" + nyc@^10.1.2: version "10.1.2" resolved "https://registry.yarnpkg.com/nyc/-/nyc-10.1.2.tgz#ea7acaa20a235210101604f4e7d56d28453b0274" @@ -4472,6 +5082,17 @@ optimist@~0.3, optimist@~0.3.5: dependencies: wordwrap "~0.0.2" +optionator@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" @@ -4522,7 +5143,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4533,6 +5154,10 @@ osenv@0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -4599,6 +5224,10 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + parse5@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" @@ -4641,7 +5270,7 @@ path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" -path-is-absolute@^1.0.0: +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -4713,6 +5342,12 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + portfinder@^1.0.9, portfinder@~1.0.12: version "1.0.13" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" @@ -5011,6 +5646,10 @@ postcss@^6.0.1: source-map "^0.5.6" supports-color "^4.1.0" +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + prepend-http@^1.0.0, prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" @@ -5030,6 +5669,17 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" +pretty-format@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.0.2.tgz#76adcebd836c41ccd2e6b626e70f63050d2a3534" + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + +private@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -5285,6 +5935,10 @@ regenerator-runtime@^0.10.0: version "0.10.3" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e" +regenerator-runtime@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + regex-cache@^0.4.2: version "0.4.3" resolved "http://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" @@ -5427,6 +6081,10 @@ resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + resolve@^1.1.6, resolve@^1.1.7: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" @@ -5494,13 +6152,7 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" -rxjs@^5.0.0-beta.11: - version "5.4.3" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" - dependencies: - symbol-observable "^1.0.1" - -rxjs@^5.0.1, rxjs@^5.4.0: +rxjs@^5.0.0-beta.11, rxjs@^5.0.1, rxjs@^5.4.0: version "5.4.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7" dependencies: @@ -5519,6 +6171,20 @@ sander@^0.5.0: mkdirp "^0.5.1" rimraf "^2.5.2" +sane@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.0.0.tgz#99cb79f21f4a53a69d4d0cd957c2db04024b8eb2" + dependencies: + anymatch "^1.3.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + optionalDependencies: + fsevents "^1.1.1" + sass-graph@^2.1.1: version "2.2.4" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" @@ -5552,7 +6218,7 @@ sax@0.6.x: version "0.6.1" resolved "https://registry.yarnpkg.com/sax/-/sax-0.6.1.tgz#563b19c7c1de892e09bfc4f2fc30e3c27f0952b9" -sax@>=0.6.0, sax@~1.2.1: +sax@>=0.6.0, sax@^1.2.1, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -5711,6 +6377,10 @@ shell-quote@^1.4.3: array-reduce "~0.0.0" jsonify "~0.0.0" +shellwords@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + signal-exit@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-2.1.2.tgz#375879b1f92ebc3b334480d038dc546a6d558564" @@ -5725,6 +6395,10 @@ silent-error@^1.0.0: dependencies: debug "^2.2.0" +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" @@ -5842,19 +6516,25 @@ source-map-loader@^0.2.0: loader-utils "~0.2.2" source-map "~0.1.33" -source-map-support@^0.4.0, source-map-support@^0.4.2, source-map-support@~0.4.0: +source-map-support@^0.4.0, source-map-support@^0.4.2, source-map-support@^0.4.4, source-map-support@~0.4.0: version "0.4.14" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef" dependencies: source-map "^0.5.6" +source-map-support@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.17.tgz#6f2150553e6375375d0ccb3180502b78c18ba430" + dependencies: + source-map "^0.5.6" + source-map@0.1.x, source-map@~0.1.33, source-map@~0.1.7: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" dependencies: amdefine ">=0.0.4" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -5988,6 +6668,13 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -6023,16 +6710,22 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@3.0.0, strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" dependencies: is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -6118,6 +6811,10 @@ symbol-observable@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" +symbol-tree@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" + tapable@^0.2.5, tapable@~0.2.5: version "0.2.6" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" @@ -6188,10 +6885,24 @@ test-exclude@^3.3.0: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" +test-exclude@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + text-extensions@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.4.0.tgz#c385d2e80879fe6ef97893e1709d88d9453726e9" +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + through2@^2.0.0, through2@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" @@ -6235,6 +6946,10 @@ tmp@^0.0.31: dependencies: os-tmpdir "~1.0.1" +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" @@ -6247,16 +6962,24 @@ to-fast-properties@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + toposort@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.3.tgz#f02cd8a74bd8be2fc0e98611c3bacb95a171869c" -tough-cookie@~2.3.0: +tough-cookie@^2.3.2, tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" dependencies: punycode "^1.4.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -6269,6 +6992,21 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +ts-jest@^21.0.0: + version "21.0.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-21.0.0.tgz#261c0b90270bfaa57c8d9ef2a0b5f3a41efc38e9" + dependencies: + babel-core "^6.24.1" + babel-plugin-istanbul "^4.1.4" + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-preset-jest "^21.0.0" + fs-extra "^4.0.0" + jest-config "^21.0.0" + jest-util "^21.0.0" + pkg-dir "^2.0.0" + source-map-support "^0.4.4" + yargs "^8.0.1" + ts-node@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.2.0.tgz#9814f0c0141784900cf12fef1197ad4b7f4d23d1" @@ -6344,6 +7082,12 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -6571,6 +7315,16 @@ walk-sync@^0.3.1: ensure-posix-path "^1.0.0" matcher-collection "^1.0.0" +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + watchpack@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" @@ -6614,6 +7368,14 @@ webdriver-manager@^12.0.6: semver "^5.3.0" xml2js "^0.4.17" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + +webidl-conversions@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + webpack-dev-middleware@^1.10.2: version "1.11.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" @@ -6701,6 +7463,19 @@ websocket-extensions@>=0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" +whatwg-encoding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + dependencies: + iconv-lite "0.4.13" + +whatwg-url@^4.3.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + when@~3.6.x: version "3.6.4" resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" @@ -6717,18 +7492,12 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1, which@^1.2.1, which@^1.2.4, which@^1.2.9: +which@1, which@^1.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.4, which@^1.2.9: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: isexe "^1.1.1" -which@^1.2.10: - version "1.3.0" - resolved "https://registry.npmjs.org/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" @@ -6753,6 +7522,17 @@ wordwrap@0.0.2, wordwrap@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +worker-farm@^1.3.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" @@ -6821,6 +7601,10 @@ xml-char-classes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d" +xml-name-validator@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + xml2js@0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.4.tgz#3111010003008ae19240eba17497b57c729c555d" @@ -6849,7 +7633,7 @@ xmlhttprequest-ssl@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" -xtend@^4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -6945,6 +7729,24 @@ yargs@^8.0.1: y18n "^3.2.1" yargs-parser "^7.0.0" +yargs@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.0.tgz#efe5b1ad3f94bdc20423411b90628eeec0b25f3c" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"