From 6004afb39571e0a7e641b9b70d5946fc1a16ea04 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Thu, 30 May 2019 22:00:01 +0200 Subject: [PATCH 1/6] feat(store): add ngrx-store-freeze migration --- modules/data/schematics-core/index.ts | 1 + .../data/schematics-core/utility/ast-utils.ts | 58 ++++- .../data/schematics-core/utility/change.ts | 27 +- modules/effects/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../effects/schematics-core/utility/change.ts | 27 +- modules/entity/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../entity/schematics-core/utility/change.ts | 27 +- modules/router-store/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../schematics-core/utility/change.ts | 27 +- modules/schematics-core/index.ts | 1 + modules/schematics-core/utility/ast-utils.ts | 58 ++++- modules/schematics-core/utility/change.ts | 27 +- modules/schematics/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../schematics-core/utility/change.ts | 27 +- .../store-devtools/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../schematics-core/utility/change.ts | 27 +- .../{8_0_0 => 8_0_0-beta}/index.spec.ts | 2 +- .../migrations/{8_0_0 => 8_0_0-beta}/index.ts | 0 .../store/migrations/8_0_0-rc/index.spec.ts | 241 ++++++++++++++++++ modules/store/migrations/8_0_0-rc/index.ts | 226 ++++++++++++++++ modules/store/migrations/migration.json | 9 +- modules/store/schematics-core/index.ts | 1 + .../schematics-core/utility/ast-utils.ts | 58 ++++- .../store/schematics-core/utility/change.ts | 27 +- 29 files changed, 1051 insertions(+), 115 deletions(-) rename modules/store/migrations/{8_0_0 => 8_0_0-beta}/index.spec.ts (98%) rename modules/store/migrations/{8_0_0 => 8_0_0-beta}/index.ts (100%) create mode 100644 modules/store/migrations/8_0_0-rc/index.spec.ts create mode 100644 modules/store/migrations/8_0_0-rc/index.ts diff --git a/modules/data/schematics-core/index.ts b/modules/data/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/data/schematics-core/index.ts +++ b/modules/data/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/data/schematics-core/utility/ast-utils.ts b/modules/data/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/data/schematics-core/utility/ast-utils.ts +++ b/modules/data/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/data/schematics-core/utility/change.ts b/modules/data/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/data/schematics-core/utility/change.ts +++ b/modules/data/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/effects/schematics-core/index.ts b/modules/effects/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/effects/schematics-core/index.ts +++ b/modules/effects/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/effects/schematics-core/utility/ast-utils.ts b/modules/effects/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/effects/schematics-core/utility/ast-utils.ts +++ b/modules/effects/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/effects/schematics-core/utility/change.ts b/modules/effects/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/effects/schematics-core/utility/change.ts +++ b/modules/effects/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/entity/schematics-core/index.ts b/modules/entity/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/entity/schematics-core/index.ts +++ b/modules/entity/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/entity/schematics-core/utility/ast-utils.ts b/modules/entity/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/entity/schematics-core/utility/ast-utils.ts +++ b/modules/entity/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/entity/schematics-core/utility/change.ts b/modules/entity/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/entity/schematics-core/utility/change.ts +++ b/modules/entity/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/router-store/schematics-core/index.ts b/modules/router-store/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/router-store/schematics-core/index.ts +++ b/modules/router-store/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/router-store/schematics-core/utility/ast-utils.ts b/modules/router-store/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/router-store/schematics-core/utility/ast-utils.ts +++ b/modules/router-store/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/router-store/schematics-core/utility/change.ts b/modules/router-store/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/router-store/schematics-core/utility/change.ts +++ b/modules/router-store/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/schematics-core/index.ts b/modules/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/schematics-core/index.ts +++ b/modules/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/schematics-core/utility/ast-utils.ts b/modules/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/schematics-core/utility/ast-utils.ts +++ b/modules/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/schematics-core/utility/change.ts b/modules/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/schematics-core/utility/change.ts +++ b/modules/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/schematics/schematics-core/index.ts b/modules/schematics/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/schematics/schematics-core/index.ts +++ b/modules/schematics/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/schematics/schematics-core/utility/ast-utils.ts b/modules/schematics/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/schematics/schematics-core/utility/ast-utils.ts +++ b/modules/schematics/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/schematics/schematics-core/utility/change.ts b/modules/schematics/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/schematics/schematics-core/utility/change.ts +++ b/modules/schematics/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/store-devtools/schematics-core/index.ts b/modules/store-devtools/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/store-devtools/schematics-core/index.ts +++ b/modules/store-devtools/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/store-devtools/schematics-core/utility/ast-utils.ts b/modules/store-devtools/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/store-devtools/schematics-core/utility/ast-utils.ts +++ b/modules/store-devtools/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/store-devtools/schematics-core/utility/change.ts b/modules/store-devtools/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/store-devtools/schematics-core/utility/change.ts +++ b/modules/store-devtools/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } diff --git a/modules/store/migrations/8_0_0/index.spec.ts b/modules/store/migrations/8_0_0-beta/index.spec.ts similarity index 98% rename from modules/store/migrations/8_0_0/index.spec.ts rename to modules/store/migrations/8_0_0-beta/index.spec.ts index 97cee4dd7b..1dd5e627b1 100644 --- a/modules/store/migrations/8_0_0/index.spec.ts +++ b/modules/store/migrations/8_0_0-beta/index.spec.ts @@ -6,7 +6,7 @@ import { import * as path from 'path'; import { createPackageJson } from '../../../schematics-core/testing/create-package'; -describe('Store Migration 8_0_0', () => { +describe('Store Migration 8_0_0 beta', () => { let appTree: UnitTestTree; const collectionPath = path.join(__dirname, '../migration.json'); const pkgName = 'store'; diff --git a/modules/store/migrations/8_0_0/index.ts b/modules/store/migrations/8_0_0-beta/index.ts similarity index 100% rename from modules/store/migrations/8_0_0/index.ts rename to modules/store/migrations/8_0_0-beta/index.ts diff --git a/modules/store/migrations/8_0_0-rc/index.spec.ts b/modules/store/migrations/8_0_0-rc/index.spec.ts new file mode 100644 index 0000000000..9079caed49 --- /dev/null +++ b/modules/store/migrations/8_0_0-rc/index.spec.ts @@ -0,0 +1,241 @@ +import { normalize } from '@angular-devkit/core'; +import { EmptyTree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; + +describe('Migration to version 8.0.0 rc', () => { + describe('removes the usage of the storeFreeze meta-reducer', () => { + /* tslint:disable */ + const fixtures = [ + { + description: 'removes the ngrx-store-freeze import', + input: `import { storeFreeze } from 'ngrx-store-freeze';`, + expected: ``, + }, + { + description: 'removes the usage of storeFeeze', + input: `import { storeFreeze } from 'ngrx-store-freeze'; + const metaReducers = environment.production ? [] : [storeFreeze]`, + expected: ` + const metaReducers = environment.production ? [] : []`, + }, + { + description: + 'removes the usage of storeFeeze with an appending meta-reducer', + input: `import { storeFreeze } from 'ngrx-store-freeze'; + const metaReducers = environment.production ? [] : [storeFreeze, foo]`, + expected: ` + const metaReducers = environment.production ? [] : [foo]`, + }, + { + description: + 'removes the usage of storeFeeze with an prepending meta-reducer', + input: `import { storeFreeze } from 'ngrx-store-freeze'; + const metaReducers = environment.production ? [] : [foo, storeFreeze]`, + expected: ` + const metaReducers = environment.production ? [] : [foo]`, + }, + { + description: 'removes the usage of storeFeeze in between meta-reducers', + input: `import { storeFreeze } from 'ngrx-store-freeze'; + const metaReducers = environment.production ? [] : [foo, storeFreeze, bar]`, + expected: ` + const metaReducers = environment.production ? [] : [foo, bar]`, + }, + ]; + /* tslint:enable */ + + const reducerPath = normalize('reducers/index.ts'); + + fixtures.forEach(async ({ description, input, expected }) => { + it(description, async () => { + const tree = new UnitTestTree(new EmptyTree()); + // we need a package.json, it will throw otherwise because we're trying to remove ngrx-store-freeze as a dep + tree.create('/package.json', JSON.stringify({})); + tree.create(reducerPath, input); + + const schematicRunner = createSchematicsRunner(); + schematicRunner.runSchematic('ngrx-store-migration-03', {}, tree); + await schematicRunner.engine.executePostTasks().toPromise(); + + const actual = tree.readContent(reducerPath); + expect(actual).toBe(expected); + }); + }); + }); + + describe('StoreModule.forRoot()', () => { + /* tslint:disable */ + const fixtures = [ + { + description: + 'enables strictStateImmutability and strictActionImmutability runtime checks with no store config', + input: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + isStoreFreezeUsed: true, + expected: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true }}), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + }, + { + description: + 'enables strictStateImmutability and strictActionImmutability runtime checks with store config', + input: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { metaReducers }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + isStoreFreezeUsed: true, + expected: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { metaReducers, runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true } }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + }, + { + description: + 'enables strictStateImmutability and strictActionImmutability runtime checks with an empty store config', + input: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + isStoreFreezeUsed: true, + expected: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true } }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + }, + { + description: + 'does not add runtime checks when store-freeze was not used', + input: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { metaReducers }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + isStoreFreezeUsed: false, + expected: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { metaReducers }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + }, + ]; + /* tslint:enable */ + + const appModulePath = normalize('app.module.ts'); + + fixtures.forEach( + async ({ description, input, isStoreFreezeUsed, expected }) => { + it(description, async () => { + const tree = new UnitTestTree(new EmptyTree()); + // we need a package.json, it will throw otherwise because we're trying to remove ngrx-store-freeze as a dep + tree.create('/package.json', JSON.stringify({})); + if (isStoreFreezeUsed) { + // we need this file to "trigger" the runtime additions + tree.create( + 'reducer.ts', + 'import { storeFreeze } from "ngrx-store-freeze";' + ); + } + tree.create(appModulePath, input); + + const schematicRunner = createSchematicsRunner(); + schematicRunner.runSchematic('ngrx-store-migration-03', {}, tree); + await schematicRunner.engine.executePostTasks().toPromise(); + + const actual = tree.readContent(appModulePath); + expect(actual).toBe(expected); + }); + } + ); + }); + + describe('package.json', () => { + /* tslint:disable */ + const fixtures = [ + { + description: 'removes ngrx-store-freeze as a dependency', + input: JSON.stringify({ + dependencies: { + 'ngrx-store-freeze': '^1.0.0', + }, + }), + }, + { + description: 'removes ngrx-store-freeze as a dev dependency', + input: JSON.stringify({ + devDependencies: { + 'ngrx-store-freeze': '^1.0.0', + }, + }), + }, + { + description: 'does not throw when ngrx-store-freeze is not installed', + input: JSON.stringify({ + dependencies: {}, + devDependencies: {}, + }), + }, + ]; + /* tslint:enable */ + + const packageJsonPath = normalize('package.json'); + + fixtures.forEach(async ({ description, input }) => { + it(description, async () => { + const tree = new UnitTestTree(new EmptyTree()); + tree.create(packageJsonPath, input); + + const schematicRunner = createSchematicsRunner(); + schematicRunner.runSchematic('ngrx-store-migration-03', {}, tree); + await schematicRunner.engine.executePostTasks().toPromise(); + + const actual = tree.readContent(packageJsonPath); + expect(actual).not.toMatch(/ngrx-store-freeze/); + }); + }); + }); +}); + +function createSchematicsRunner() { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration.json') + ); + + return schematicRunner; +} diff --git a/modules/store/migrations/8_0_0-rc/index.ts b/modules/store/migrations/8_0_0-rc/index.ts new file mode 100644 index 0000000000..bca0d57d92 --- /dev/null +++ b/modules/store/migrations/8_0_0-rc/index.ts @@ -0,0 +1,226 @@ +import * as ts from 'typescript'; +import { + Rule, + chain, + Tree, + SchematicContext, + SchematicsException, +} from '@angular-devkit/schematics'; +import { + createChangeRecorder, + RemoveChange, + InsertChange, +} from '@ngrx/store/schematics-core'; +import { Path } from '@angular-devkit/core'; + +function removeNgrxStoreFreezeImport(): Rule { + return (tree: Tree) => { + // only add runtime checks when ngrx-store-freeze is used + removeUsages(tree) && insertRuntimeChecks(tree); + }; +} + +function removeNgRxStoreFreezePackage(): Rule { + return (tree: Tree) => { + const pkgPath = '/package.json'; + const buffer = tree.read(pkgPath); + if (buffer === null) { + throw new SchematicsException('Could not read package.json'); + } + const content = buffer.toString(); + const pkg = JSON.parse(content); + + if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) { + throw new SchematicsException('Error reading package.json'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + if (pkg[category] && pkg[category]['ngrx-store-freeze']) { + delete pkg[category]['ngrx-store-freeze']; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + return tree; + }; +} + +export default function(): Rule { + return chain([removeNgrxStoreFreezeImport(), removeNgRxStoreFreezePackage()]); +} + +function removeUsages(tree: Tree) { + let ngrxStoreFreezeIsUsed = false; + + 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 importRemovements = findStoreFreezeImportsToRemove(sourceFile, path); + if (importRemovements.length === 0) { + return []; + } + + ngrxStoreFreezeIsUsed = true; + const usageReplacements = findStoreFreezeUsagesToRemove(sourceFile, path); + const runtimeChecksInserts = findRuntimeCHecksToInsert(sourceFile, path); + + const changes = [ + ...importRemovements, + ...usageReplacements, + ...runtimeChecksInserts, + ]; + const recorder = createChangeRecorder(tree, path, changes); + tree.commitUpdate(recorder); + }); + + return ngrxStoreFreezeIsUsed; +} + +function insertRuntimeChecks(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 runtimeChecksInserts = findRuntimeCHecksToInsert(sourceFile, path); + const recorder = createChangeRecorder(tree, path, runtimeChecksInserts); + tree.commitUpdate(recorder); + }); +} + +function findStoreFreezeImportsToRemove(sourceFile: ts.SourceFile, path: Path) { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter(({ moduleSpecifier }) => { + return ( + moduleSpecifier.getText(sourceFile) === `'ngrx-store-freeze'` || + moduleSpecifier.getText(sourceFile) === `"ngrx-store-freeze"` + ); + }); + + const removements = imports.map( + i => new RemoveChange(path, i.getStart(sourceFile), i.getEnd()) + ); + return removements; +} + +function findStoreFreezeUsagesToRemove(sourceFile: ts.SourceFile, path: Path) { + let changes: (RemoveChange | InsertChange)[] = []; + ts.forEachChild(sourceFile, node => crawl(node, changes)); + return changes; + + function crawl(node: ts.Node, changes: (RemoveChange | InsertChange)[]) { + if (!ts.isArrayLiteralExpression(node)) { + ts.forEachChild(node, childNode => crawl(childNode, changes)); + return; + } + + const elements = node.elements.map(elem => elem.getText(sourceFile)); + const elementsWithoutStoreFreeze = elements.filter( + elemText => elemText !== 'storeFreeze' + ); + + if (elements.length !== elementsWithoutStoreFreeze.length) { + changes.push( + new RemoveChange( + sourceFile.fileName, + node.getStart(sourceFile), + node.getEnd() + ) + ); + changes.push( + new InsertChange( + path, + node.getStart(sourceFile), + `[${elementsWithoutStoreFreeze.join(', ')}]` + ) + ); + } + } +} + +function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { + let changes: (InsertChange)[] = []; + ts.forEachChild(sourceFile, node => crawl(node, changes)); + return changes; + + function crawl(node: ts.Node, changes: (InsertChange)[]) { + if (!ts.isCallExpression(node)) { + ts.forEachChild(node, childNode => crawl(childNode, changes)); + return; + } + + const expression = node.expression; + if ( + !( + ts.isPropertyAccessExpression(expression) && + expression.expression.getText(sourceFile) === 'StoreModule' && + expression.name.getText(sourceFile) === 'forRoot' + ) + ) { + ts.forEachChild(node, childNode => crawl(childNode, changes)); + return; + } + + const runtimeChecks = `runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true }`; + + // covers StoreModule.forRoot(ROOT_REDUCERS) + if (node.arguments.length === 1) { + changes.push( + new InsertChange( + path, + node.arguments[0].getEnd(), + `, { ${runtimeChecks}}` + ) + ); + } else if (node.arguments.length === 2) { + const storeConfig = node.arguments[1]; + if (ts.isObjectLiteralExpression(storeConfig)) { + // covers StoreModule.forRoot(ROOT_REDUCERS, {}) + if (storeConfig.properties.length === 0) { + changes.push( + new InsertChange( + path, + storeConfig.getEnd() - 1, + `${runtimeChecks} ` + ) + ); + } else { + // covers StoreModule.forRoot(ROOT_REDUCERS, { metaReducers }) + const lastProperty = + storeConfig.properties[storeConfig.properties.length - 1]; + + changes.push( + new InsertChange(path, lastProperty.getEnd(), `, ${runtimeChecks}`) + ); + } + } + } + + ts.forEachChild(node, childNode => crawl(childNode, changes)); + } +} diff --git a/modules/store/migrations/migration.json b/modules/store/migrations/migration.json index 5589b42414..56d3f9c1b9 100644 --- a/modules/store/migrations/migration.json +++ b/modules/store/migrations/migration.json @@ -8,9 +8,14 @@ "factory": "./6_0_0/index" }, "ngrx-store-migration-02": { - "description": "The road to v8", + "description": "The road to v8 beta", "version": "8-beta", - "factory": "./8_0_0/index" + "factory": "./8_0_0-beta/index" + }, + "ngrx-store-migration-03": { + "description": "The road to v8 RC", + "version": "8-rc.1", + "factory": "./8_0_0-rc/index" } } } diff --git a/modules/store/schematics-core/index.ts b/modules/store/schematics-core/index.ts index 7da4fa8c2d..a3f9efbe80 100644 --- a/modules/store/schematics-core/index.ts +++ b/modules/store/schematics-core/index.ts @@ -21,6 +21,7 @@ export { addExportToModule, addImportToModule, addProviderToModule, + replaceImport, } from './utility/ast-utils'; export { diff --git a/modules/store/schematics-core/utility/ast-utils.ts b/modules/store/schematics-core/utility/ast-utils.ts index f6174fc4b1..f5d7d91dd6 100644 --- a/modules/store/schematics-core/utility/ast-utils.ts +++ b/modules/store/schematics-core/utility/ast-utils.ts @@ -7,7 +7,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import { Change, InsertChange, NoopChange } from './change'; +import { + Change, + InsertChange, + NoopChange, + createReplaceChange, + ReplaceChange, +} from './change'; +import { Path } from '@angular-devkit/core'; /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. @@ -636,3 +643,52 @@ export function insertImport( ts.SyntaxKind.StringLiteral ); } + +export function replaceImport( + sourceFile: ts.SourceFile, + path: Path, + importFrom: string, + importAsIs: string, + importToBe: string +): ReplaceChange[] { + const imports = sourceFile.statements + .filter(ts.isImportDeclaration) + .filter( + ({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile) === `'${importFrom}'` || + moduleSpecifier.getText(sourceFile) === `"${importFrom}"` + ); + + if (imports.length === 0) { + return []; + } + + const changes = imports + .map(p => (p.importClause!.namedBindings! as ts.NamedImports).elements) + .reduce((imports, curr) => imports.concat(curr), [] as ts.ImportSpecifier[]) + .map(specifier => { + if (!ts.isImportSpecifier(specifier)) { + return { hit: false }; + } + + if (specifier.name.text === importAsIs) { + return { hit: true, specifier, text: specifier.name.text }; + } + + // if import is renamed + if ( + specifier.propertyName && + specifier.propertyName.text === importAsIs + ) { + return { hit: true, specifier, text: specifier.propertyName.text }; + } + + return { hit: false }; + }) + .filter(({ hit }) => hit) + .map(({ specifier, text }) => + createReplaceChange(sourceFile, path, specifier!, text!, importToBe) + ); + + return changes; +} diff --git a/modules/store/schematics-core/utility/change.ts b/modules/store/schematics-core/utility/change.ts index 5dff73e3b6..026e7a7ba6 100644 --- a/modules/store/schematics-core/utility/change.ts +++ b/modules/store/schematics-core/utility/change.ts @@ -77,22 +77,18 @@ export class RemoveChange implements Change { order: number; description: string; - constructor( - public path: string, - private pos: number, - private toRemove: string - ) { - if (pos < 0) { + constructor(public path: string, public pos: number, public end: number) { + if (pos < 0 || end < 0) { throw new Error('Negative positions are invalid'); } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.description = `Removed text in position ${pos} to ${end} of ${path}`; this.order = pos; } apply(host: Host): Promise { return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); + const suffix = content.substring(this.end); // TODO: throw error if toRemove doesn't match removed string. return host.write(this.path, `${prefix}${suffix}`); @@ -109,7 +105,7 @@ export class ReplaceChange implements Change { constructor( public path: string, - private pos: number, + public pos: number, public oldText: string, public newText: string ) { @@ -151,13 +147,18 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, path: Path, - changes: ReplaceChange[] + changes: Change[] ): UpdateRecorder { 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); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } else if (change instanceof RemoveChange) { + recorder.remove(change.pos, change.end - change.pos); + } else if (change instanceof ReplaceChange) { + recorder.remove(change.pos, change.oldText.length); + recorder.insertLeft(change.pos, change.newText); + } } return recorder; } From a3c62bcf6a2db245f7f1d332741a0f2bc7444868 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Thu, 30 May 2019 22:13:37 +0200 Subject: [PATCH 2/6] build: add @angular-devkit/core as dep --- modules/store/migrations/8_0_0-rc/index.ts | 3 +-- modules/store/migrations/BUILD | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/store/migrations/8_0_0-rc/index.ts b/modules/store/migrations/8_0_0-rc/index.ts index bca0d57d92..7261aaca5e 100644 --- a/modules/store/migrations/8_0_0-rc/index.ts +++ b/modules/store/migrations/8_0_0-rc/index.ts @@ -3,15 +3,14 @@ import { Rule, chain, Tree, - SchematicContext, SchematicsException, } from '@angular-devkit/schematics'; +import { Path } from '@angular-devkit/core'; import { createChangeRecorder, RemoveChange, InsertChange, } from '@ngrx/store/schematics-core'; -import { Path } from '@angular-devkit/core'; function removeNgrxStoreFreezeImport(): Rule { return (tree: Tree) => { diff --git a/modules/store/migrations/BUILD b/modules/store/migrations/BUILD index 4fb1d66805..d8b9d91860 100644 --- a/modules/store/migrations/BUILD +++ b/modules/store/migrations/BUILD @@ -16,6 +16,7 @@ ts_library( module_name = "@ngrx/store/migrations", deps = [ "//modules/store/schematics-core", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//typescript", ], From a7caed957d3d53836cc4081a56b38f9a50626f81 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 31 May 2019 15:11:41 +0200 Subject: [PATCH 3/6] refactor: extract redundent code --- modules/data/schematics-core/index.ts | 2 + .../data/schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/effects/schematics-core/index.ts | 2 + .../effects/schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/entity/schematics-core/index.ts | 2 + .../entity/schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/router-store/schematics-core/index.ts | 2 + .../schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/schematics-core/index.ts | 2 + modules/schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/schematics/schematics-core/index.ts | 2 + .../schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ .../store-devtools/schematics-core/index.ts | 2 + .../schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ modules/store/migrations/8_0_0-rc/index.ts | 131 +++++++----------- modules/store/schematics-core/index.ts | 2 + .../store/schematics-core/utility/change.ts | 2 +- .../schematics-core/utility/visit-utils.ts | 33 +++++ 25 files changed, 338 insertions(+), 89 deletions(-) create mode 100644 modules/data/schematics-core/utility/visit-utils.ts create mode 100644 modules/effects/schematics-core/utility/visit-utils.ts create mode 100644 modules/entity/schematics-core/utility/visit-utils.ts create mode 100644 modules/router-store/schematics-core/utility/visit-utils.ts create mode 100644 modules/schematics-core/utility/visit-utils.ts create mode 100644 modules/schematics/schematics-core/utility/visit-utils.ts create mode 100644 modules/store-devtools/schematics-core/utility/visit-utils.ts create mode 100644 modules/store/schematics-core/utility/visit-utils.ts diff --git a/modules/data/schematics-core/index.ts b/modules/data/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/data/schematics-core/index.ts +++ b/modules/data/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/data/schematics-core/utility/change.ts b/modules/data/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/data/schematics-core/utility/change.ts +++ b/modules/data/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/data/schematics-core/utility/visit-utils.ts b/modules/data/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/data/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/effects/schematics-core/index.ts b/modules/effects/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/effects/schematics-core/index.ts +++ b/modules/effects/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/effects/schematics-core/utility/change.ts b/modules/effects/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/effects/schematics-core/utility/change.ts +++ b/modules/effects/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/effects/schematics-core/utility/visit-utils.ts b/modules/effects/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/effects/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/entity/schematics-core/index.ts b/modules/entity/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/entity/schematics-core/index.ts +++ b/modules/entity/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/entity/schematics-core/utility/change.ts b/modules/entity/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/entity/schematics-core/utility/change.ts +++ b/modules/entity/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/entity/schematics-core/utility/visit-utils.ts b/modules/entity/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/entity/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/router-store/schematics-core/index.ts b/modules/router-store/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/router-store/schematics-core/index.ts +++ b/modules/router-store/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/router-store/schematics-core/utility/change.ts b/modules/router-store/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/router-store/schematics-core/utility/change.ts +++ b/modules/router-store/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/router-store/schematics-core/utility/visit-utils.ts b/modules/router-store/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/router-store/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/schematics-core/index.ts b/modules/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/schematics-core/index.ts +++ b/modules/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/schematics-core/utility/change.ts b/modules/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/schematics-core/utility/change.ts +++ b/modules/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/schematics-core/utility/visit-utils.ts b/modules/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/schematics/schematics-core/index.ts b/modules/schematics/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/schematics/schematics-core/index.ts +++ b/modules/schematics/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/schematics/schematics-core/utility/change.ts b/modules/schematics/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/schematics/schematics-core/utility/change.ts +++ b/modules/schematics/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/schematics/schematics-core/utility/visit-utils.ts b/modules/schematics/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/schematics/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/store-devtools/schematics-core/index.ts b/modules/store-devtools/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/store-devtools/schematics-core/index.ts +++ b/modules/store-devtools/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/store-devtools/schematics-core/utility/change.ts b/modules/store-devtools/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/store-devtools/schematics-core/utility/change.ts +++ b/modules/store-devtools/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/store-devtools/schematics-core/utility/visit-utils.ts b/modules/store-devtools/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..cabf341ceb --- /dev/null +++ b/modules/store-devtools/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result: Result | undefined + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} diff --git a/modules/store/migrations/8_0_0-rc/index.ts b/modules/store/migrations/8_0_0-rc/index.ts index 7261aaca5e..5af3b71bbe 100644 --- a/modules/store/migrations/8_0_0-rc/index.ts +++ b/modules/store/migrations/8_0_0-rc/index.ts @@ -5,17 +5,18 @@ import { Tree, SchematicsException, } from '@angular-devkit/schematics'; -import { Path } from '@angular-devkit/core'; import { createChangeRecorder, RemoveChange, InsertChange, + visitTSSourceFiles, } from '@ngrx/store/schematics-core'; -function removeNgrxStoreFreezeImport(): Rule { +function replaceWithRuntimeChecks(): Rule { return (tree: Tree) => { // only add runtime checks when ngrx-store-freeze is used - removeUsages(tree) && insertRuntimeChecks(tree); + visitTSSourceFiles(tree, removeUsages) && + visitTSSourceFiles(tree, insertRuntimeChecks); }; } @@ -47,71 +48,39 @@ function removeNgRxStoreFreezePackage(): Rule { } export default function(): Rule { - return chain([removeNgrxStoreFreezeImport(), removeNgRxStoreFreezePackage()]); + return chain([removeNgRxStoreFreezePackage(), replaceWithRuntimeChecks()]); } -function removeUsages(tree: Tree) { - let ngrxStoreFreezeIsUsed = false; - - 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 importRemovements = findStoreFreezeImportsToRemove(sourceFile, path); - if (importRemovements.length === 0) { - return []; - } +function removeUsages( + sourceFile: ts.SourceFile, + tree: Tree, + ngrxStoreFreezeIsUsed?: boolean +) { + const importRemovements = findStoreFreezeImportsToRemove(sourceFile); + if (importRemovements.length === 0) { + return ngrxStoreFreezeIsUsed; + } - ngrxStoreFreezeIsUsed = true; - const usageReplacements = findStoreFreezeUsagesToRemove(sourceFile, path); - const runtimeChecksInserts = findRuntimeCHecksToInsert(sourceFile, path); + const usageReplacements = findStoreFreezeUsagesToRemove(sourceFile); - const changes = [ - ...importRemovements, - ...usageReplacements, - ...runtimeChecksInserts, - ]; - const recorder = createChangeRecorder(tree, path, changes); - tree.commitUpdate(recorder); - }); + const changes = [...importRemovements, ...usageReplacements]; + const recorder = createChangeRecorder(tree, sourceFile.fileName, changes); + tree.commitUpdate(recorder); - return ngrxStoreFreezeIsUsed; + return true; } -function insertRuntimeChecks(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 runtimeChecksInserts = findRuntimeCHecksToInsert(sourceFile, path); - const recorder = createChangeRecorder(tree, path, runtimeChecksInserts); - tree.commitUpdate(recorder); - }); +function insertRuntimeChecks(sourceFile: ts.SourceFile, tree: Tree) { + const runtimeChecksInserts = findRuntimeCHecksToInsert(sourceFile); + const recorder = createChangeRecorder( + tree, + sourceFile.fileName, + runtimeChecksInserts + ); + tree.commitUpdate(recorder); } -function findStoreFreezeImportsToRemove(sourceFile: ts.SourceFile, path: Path) { +function findStoreFreezeImportsToRemove(sourceFile: ts.SourceFile) { const imports = sourceFile.statements .filter(ts.isImportDeclaration) .filter(({ moduleSpecifier }) => { @@ -122,21 +91,21 @@ function findStoreFreezeImportsToRemove(sourceFile: ts.SourceFile, path: Path) { }); const removements = imports.map( - i => new RemoveChange(path, i.getStart(sourceFile), i.getEnd()) + i => + new RemoveChange(sourceFile.fileName, i.getStart(sourceFile), i.getEnd()) ); return removements; } -function findStoreFreezeUsagesToRemove(sourceFile: ts.SourceFile, path: Path) { +function findStoreFreezeUsagesToRemove(sourceFile: ts.SourceFile) { let changes: (RemoveChange | InsertChange)[] = []; - ts.forEachChild(sourceFile, node => crawl(node, changes)); + ts.forEachChild(sourceFile, crawl); return changes; - function crawl(node: ts.Node, changes: (RemoveChange | InsertChange)[]) { - if (!ts.isArrayLiteralExpression(node)) { - ts.forEachChild(node, childNode => crawl(childNode, changes)); - return; - } + function crawl(node: ts.Node) { + ts.forEachChild(node, crawl); + + if (!ts.isArrayLiteralExpression(node)) return; const elements = node.elements.map(elem => elem.getText(sourceFile)); const elementsWithoutStoreFreeze = elements.filter( @@ -153,7 +122,7 @@ function findStoreFreezeUsagesToRemove(sourceFile: ts.SourceFile, path: Path) { ); changes.push( new InsertChange( - path, + sourceFile.fileName, node.getStart(sourceFile), `[${elementsWithoutStoreFreeze.join(', ')}]` ) @@ -162,16 +131,15 @@ function findStoreFreezeUsagesToRemove(sourceFile: ts.SourceFile, path: Path) { } } -function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { +function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile) { let changes: (InsertChange)[] = []; - ts.forEachChild(sourceFile, node => crawl(node, changes)); + ts.forEachChild(sourceFile, crawl); return changes; - function crawl(node: ts.Node, changes: (InsertChange)[]) { - if (!ts.isCallExpression(node)) { - ts.forEachChild(node, childNode => crawl(childNode, changes)); - return; - } + function crawl(node: ts.Node) { + ts.forEachChild(node, crawl); + + if (!ts.isCallExpression(node)) return; const expression = node.expression; if ( @@ -181,7 +149,6 @@ function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { expression.name.getText(sourceFile) === 'forRoot' ) ) { - ts.forEachChild(node, childNode => crawl(childNode, changes)); return; } @@ -191,7 +158,7 @@ function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { if (node.arguments.length === 1) { changes.push( new InsertChange( - path, + sourceFile.fileName, node.arguments[0].getEnd(), `, { ${runtimeChecks}}` ) @@ -203,7 +170,7 @@ function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { if (storeConfig.properties.length === 0) { changes.push( new InsertChange( - path, + sourceFile.fileName, storeConfig.getEnd() - 1, `${runtimeChecks} ` ) @@ -214,12 +181,14 @@ function findRuntimeCHecksToInsert(sourceFile: ts.SourceFile, path: Path) { storeConfig.properties[storeConfig.properties.length - 1]; changes.push( - new InsertChange(path, lastProperty.getEnd(), `, ${runtimeChecks}`) + new InsertChange( + sourceFile.fileName, + lastProperty.getEnd(), + `, ${runtimeChecks}` + ) ); } } } - - ts.forEachChild(node, childNode => crawl(childNode, changes)); } } diff --git a/modules/store/schematics-core/index.ts b/modules/store/schematics-core/index.ts index a3f9efbe80..82f2021e08 100644 --- a/modules/store/schematics-core/index.ts +++ b/modules/store/schematics-core/index.ts @@ -72,3 +72,5 @@ export { parseName } from './utility/parse-name'; export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; + +export { visitTSSourceFiles } from './utility/visit-utils'; diff --git a/modules/store/schematics-core/utility/change.ts b/modules/store/schematics-core/utility/change.ts index 026e7a7ba6..51f451b8ba 100644 --- a/modules/store/schematics-core/utility/change.ts +++ b/modules/store/schematics-core/utility/change.ts @@ -146,7 +146,7 @@ export function createReplaceChange( export function createChangeRecorder( tree: Tree, - path: Path, + path: string, changes: Change[] ): UpdateRecorder { const recorder = tree.beginUpdate(path); diff --git a/modules/store/schematics-core/utility/visit-utils.ts b/modules/store/schematics-core/utility/visit-utils.ts new file mode 100644 index 0000000000..fc33b88227 --- /dev/null +++ b/modules/store/schematics-core/utility/visit-utils.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { Tree } from '@angular-devkit/schematics'; + +export function visitTSSourceFiles( + tree: Tree, + visitor: ( + sourceFile: ts.SourceFile, + tree: Tree, + result?: Result + ) => Result | undefined +): Result | undefined { + let result: Result | undefined = undefined; + + tree.visit(path => { + if (!path.endsWith('.ts')) { + return; + } + + const sourceFile = ts.createSourceFile( + path, + tree.read(path)!.toString(), + ts.ScriptTarget.Latest + ); + + if (sourceFile.isDeclarationFile) { + return; + } + + result = visitor(sourceFile, tree, result); + }); + + return result; +} From 2b4082260ef603120b743d4dad8c9499da14b420 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 31 May 2019 15:43:24 +0200 Subject: [PATCH 4/6] test(store): add test with storeconfig ending with a comma --- .../store/migrations/8_0_0-rc/index.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/store/migrations/8_0_0-rc/index.spec.ts b/modules/store/migrations/8_0_0-rc/index.spec.ts index 9079caed49..93c06f4e3a 100644 --- a/modules/store/migrations/8_0_0-rc/index.spec.ts +++ b/modules/store/migrations/8_0_0-rc/index.spec.ts @@ -111,6 +111,31 @@ describe('Migration to version 8.0.0 rc', () => { }) export class AppModule {}`, }, + { + description: + 'enables strictStateImmutability and strictActionImmutability runtime checks with store config ending with a comma', + input: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { + metaReducers, + }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + isStoreFreezeUsed: true, + expected: ` + @NgModule({ + imports: [ + StoreModule.forRoot(ROOT_REDUCERS, { + metaReducers, runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true }, + }), + ], + bootstrap: [AppComponent], + }) + export class AppModule {}`, + }, { description: 'enables strictStateImmutability and strictActionImmutability runtime checks with an empty store config', From 0318924961e4b9a0427e218e01ac2226c4d4b033 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 31 May 2019 15:48:29 +0200 Subject: [PATCH 5/6] docs: add ngrx-store-freeze migration --- projects/ngrx.io/content/guide/migration/v8.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/projects/ngrx.io/content/guide/migration/v8.md b/projects/ngrx.io/content/guide/migration/v8.md index a9e3ade2df..6249f2df8f 100644 --- a/projects/ngrx.io/content/guide/migration/v8.md +++ b/projects/ngrx.io/content/guide/migration/v8.md @@ -153,6 +153,16 @@ export const getNews: MemoizedSelector = createSelector( The return type of the `createSelectorFactory` and `createSelector` functions are now a `MemoizedSelector` instead of a `Selector`. +#### Deprecation of ngrx-store-freeze + +
+ +A migration is provided to remove the usage `ngrx-store-freeze`, remove it from the `package.json`, and to enable the built-in runtime checks `strictStateImmutability` and `strictActionImmutability`. + +
+ +With the new built-in runtime checks, the usage of the `ngrx-store-freeze` package has become obsolete. + ### @ngrx/effects #### Resubscribe on Errors From 4d1128cd0b55e0c0cb7d2db85d8b6196fd1aced9 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 31 May 2019 16:12:54 +0200 Subject: [PATCH 6/6] chore: copy schematics --- modules/data/schematics-core/utility/visit-utils.ts | 2 +- modules/effects/schematics-core/utility/visit-utils.ts | 2 +- modules/entity/schematics-core/utility/visit-utils.ts | 2 +- modules/router-store/schematics-core/utility/visit-utils.ts | 2 +- modules/schematics-core/utility/visit-utils.ts | 2 +- modules/schematics/schematics-core/utility/visit-utils.ts | 2 +- modules/store-devtools/schematics-core/utility/visit-utils.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/data/schematics-core/utility/visit-utils.ts b/modules/data/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/data/schematics-core/utility/visit-utils.ts +++ b/modules/data/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/effects/schematics-core/utility/visit-utils.ts b/modules/effects/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/effects/schematics-core/utility/visit-utils.ts +++ b/modules/effects/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/entity/schematics-core/utility/visit-utils.ts b/modules/entity/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/entity/schematics-core/utility/visit-utils.ts +++ b/modules/entity/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/router-store/schematics-core/utility/visit-utils.ts b/modules/router-store/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/router-store/schematics-core/utility/visit-utils.ts +++ b/modules/router-store/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/schematics-core/utility/visit-utils.ts b/modules/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/schematics-core/utility/visit-utils.ts +++ b/modules/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/schematics/schematics-core/utility/visit-utils.ts b/modules/schematics/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/schematics/schematics-core/utility/visit-utils.ts +++ b/modules/schematics/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined; diff --git a/modules/store-devtools/schematics-core/utility/visit-utils.ts b/modules/store-devtools/schematics-core/utility/visit-utils.ts index cabf341ceb..fc33b88227 100644 --- a/modules/store-devtools/schematics-core/utility/visit-utils.ts +++ b/modules/store-devtools/schematics-core/utility/visit-utils.ts @@ -6,7 +6,7 @@ export function visitTSSourceFiles( visitor: ( sourceFile: ts.SourceFile, tree: Tree, - result: Result | undefined + result?: Result ) => Result | undefined ): Result | undefined { let result: Result | undefined = undefined;