diff --git a/modules/store/migrations/8_0_0/index.spec.ts b/modules/store/migrations/8_0_0/index.spec.ts new file mode 100644 index 0000000000..97cee4dd7b --- /dev/null +++ b/modules/store/migrations/8_0_0/index.spec.ts @@ -0,0 +1,113 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { createPackageJson } from '../../../schematics-core/testing/create-package'; + +describe('Store Migration 8_0_0', () => { + let appTree: UnitTestTree; + const collectionPath = path.join(__dirname, '../migration.json'); + const pkgName = 'store'; + beforeEach(() => { + appTree = new UnitTestTree(Tree.empty()); + appTree.create( + '/tsconfig.json', + ` + { + "include": [**./*.ts"] + } + ` + ); + createPackageJson('', pkgName, appTree); + }); + + it(`should replace the meta reducer imports`, () => { + const contents = ` + import { + RuntimeChecks, + META_REDUCERS, + Store, + META_REDUCERS, + StoreModule, + META_REDUCERS as foo, + } from '@ngrx/store';`; + const expected = ` + import { + RuntimeChecks, + USER_PROVIDED_META_REDUCERS, + Store, + USER_PROVIDED_META_REDUCERS, + StoreModule, + USER_PROVIDED_META_REDUCERS as foo, + } from '@ngrx/store';`; + + appTree.create('./app.module.ts', contents); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-02`, + {}, + appTree + ); + const file = newTree.readContent('app.module.ts'); + + expect(file).toBe(expected); + }); + + it(`should replace the meta reducer assignments`, () => { + const contents = ` + @NgModule({ + imports: [ + CommonModule, + BrowserModule, + BrowserAnimationsModule, + HttpClientModule, + AuthModule, + AppRoutingModule, + StoreModule.forRoot(reducers), + ], + providers: [ + { + provide: META_REDUCERS, + useValue: [fooReducer, barReducer] + } + ] + bootstrap: [AppComponent], + }) + export class AppModule {}`; + const expected = ` + @NgModule({ + imports: [ + CommonModule, + BrowserModule, + BrowserAnimationsModule, + HttpClientModule, + AuthModule, + AppRoutingModule, + StoreModule.forRoot(reducers), + ], + providers: [ + { + provide: USER_PROVIDED_META_REDUCERS, + useValue: [fooReducer, barReducer] + } + ] + bootstrap: [AppComponent], + }) + export class AppModule {}`; + + appTree.create('./app.module.ts', contents); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-02`, + {}, + appTree + ); + const file = newTree.readContent('app.module.ts'); + + expect(file).toBe(expected); + }); +}); diff --git a/modules/store/migrations/8_0_0/index.ts b/modules/store/migrations/8_0_0/index.ts new file mode 100644 index 0000000000..b23913c749 --- /dev/null +++ b/modules/store/migrations/8_0_0/index.ts @@ -0,0 +1,120 @@ +import * as ts from 'typescript'; +import { Rule, chain, Tree } from '@angular-devkit/schematics'; +import { Path } from '@angular-devkit/core'; +import { ReplaceChange } from '@ngrx/store/schematics-core'; + +const META_REDUCERS = 'META_REDUCERS'; + +function updateMetaReducersToken(): Rule { + return (tree: Tree) => { + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + const createChange = (node: ts.Node) => + new ReplaceChange( + path, + node.getStart(sourceFile), + META_REDUCERS, + 'USER_PROVIDED_META_REDUCERS' + ); + + const changes: ReplaceChange[] = []; + changes.push( + ...findMetaReducersImportStatements(sourceFile, createChange) + ); + changes.push(...findMetaReducersAssignment(sourceFile, createChange)); + + if (changes.length < 1) { + return; + } + + const recorder = createChangeRecorder(tree, path, changes); + tree.commitUpdate(recorder); + }); + }; +} + +export default function(): Rule { + return chain([updateMetaReducersToken()]); +} + +function findMetaReducersImportStatements( + sourceFile: ts.SourceFile, + createChange: (node: ts.Node) => ReplaceChange +) { + const metaReducerImports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter(isNgRxStoreImport) + .map(p => + (p.importClause!.namedBindings! as ts.NamedImports).elements.filter( + isMetaReducersImportSpecifier + ) + ) + .reduce((imports, curr) => imports.concat(curr), []); + + const changes = metaReducerImports.map(createChange); + return changes; + + function isNgRxStoreImport(importDeclaration: ts.ImportDeclaration) { + return ( + importDeclaration.moduleSpecifier.getText(sourceFile) === "'@ngrx/store'" + ); + } + + function isMetaReducersImportSpecifier(importSpecifier: ts.ImportSpecifier) { + const isImport = () => importSpecifier.name.text === META_REDUCERS; + const isRenamedImport = () => + importSpecifier.propertyName && + importSpecifier.propertyName.text === META_REDUCERS; + + return ( + ts.isImportSpecifier(importSpecifier) && (isImport() || isRenamedImport()) + ); + } +} + +function findMetaReducersAssignment( + sourceFile: ts.SourceFile, + createChange: (node: ts.Node) => ReplaceChange +) { + let changes: ReplaceChange[] = []; + ts.forEachChild(sourceFile, node => findMetaReducers(node, changes)); + return changes; + + function findMetaReducers(node: ts.Node, changes: ReplaceChange[]) { + if ( + ts.isPropertyAssignment(node) && + node.initializer.getText(sourceFile) === META_REDUCERS + ) { + changes.push(createChange(node.initializer)); + } + + ts.forEachChild(node, childNode => findMetaReducers(childNode, changes)); + } +} + +function createChangeRecorder( + tree: Tree, + path: Path, + changes: ReplaceChange[] +) { + const recorder = tree.beginUpdate(path); + for (const change of changes) { + const action = change; + recorder.remove(action.pos, action.oldText.length); + recorder.insertLeft(action.pos, action.newText); + } + return recorder; +} diff --git a/modules/store/migrations/BUILD b/modules/store/migrations/BUILD index b5360bf8af..4fb1d66805 100644 --- a/modules/store/migrations/BUILD +++ b/modules/store/migrations/BUILD @@ -17,6 +17,7 @@ ts_library( deps = [ "//modules/store/schematics-core", "@npm//@angular-devkit/schematics", + "@npm//typescript", ], ) diff --git a/modules/store/migrations/migration.json b/modules/store/migrations/migration.json index 6a0dc2d52f..26ab6a5553 100644 --- a/modules/store/migrations/migration.json +++ b/modules/store/migrations/migration.json @@ -6,6 +6,11 @@ "description": "The road to v6", "version": "5.2", "factory": "./6_0_0/index" + }, + "ngrx-store-migration-02": { + "description": "The road to v8", + "version": "7.0", + "factory": "./8_0_0/index" } } }