forked from ngrx/platform
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component-store): add migrator for
tapResponse
Migrates any `import` of `tapResponse` from `@ngrx/component-store` to `@ngrx/operators`. Fixes ngrx#4261
- Loading branch information
1 parent
d264c56
commit aafeb36
Showing
3 changed files
with
302 additions
and
1 deletion.
There are no files selected for viewing
138 changes: 138 additions & 0 deletions
138
modules/component-store/migrations/18_0_0-beta/index.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { | ||
SchematicTestRunner, | ||
UnitTestTree, | ||
} from '@angular-devkit/schematics/testing'; | ||
import { createWorkspace } from '@ngrx/schematics-core/testing'; | ||
import { tags } from '@angular-devkit/core'; | ||
import * as path from 'path'; | ||
|
||
describe('ComponentStore Migration to 18.0.0-beta', () => { | ||
const collectionPath = path.join(__dirname, '../migration.json'); | ||
const schematicRunner = new SchematicTestRunner('schematics', collectionPath); | ||
|
||
let appTree: UnitTestTree; | ||
|
||
beforeEach(async () => { | ||
appTree = await createWorkspace(schematicRunner, appTree); | ||
}); | ||
|
||
const verifySchematic = async (input: string, output: string) => { | ||
appTree.create('main.ts', input); | ||
|
||
const tree = await schematicRunner.runSchematic( | ||
`ngrx-component-store-migration-18-beta`, | ||
{}, | ||
appTree | ||
); | ||
|
||
const actual = tree.readContent('main.ts'); | ||
|
||
expect(actual).toBe(output); | ||
}; | ||
|
||
describe('replacements', () => { | ||
it('should replace the import', async () => { | ||
const input = tags.stripIndent` | ||
import { tapResponse } from '@ngrx/component-store'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
const output = tags.stripIndent` | ||
import { tapResponse } from '@ngrx/operators'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
|
||
await verifySchematic(input, output); | ||
}); | ||
|
||
it('should also work with " in imports', async () => { | ||
const input = tags.stripIndent` | ||
import { tapResponse } from "@ngrx/component-store"; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
const output = tags.stripIndent` | ||
import { tapResponse } from '@ngrx/operators'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
await verifySchematic(input, output); | ||
}); | ||
|
||
it('should replace if multiple imports are inside an import statement', async () => { | ||
const input = tags.stripIndent` | ||
import { ComponentStore, tapResponse } from '@ngrx/component-store'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
const output = tags.stripIndent` | ||
import { ComponentStore } from '@ngrx/component-store'; | ||
import { tapResponse } from '@ngrx/operators'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
|
||
await verifySchematic(input, output); | ||
}); | ||
|
||
it('should add tapResponse to existing import', async () => { | ||
const input = tags.stripIndent` | ||
import { ComponentStore, tapResponse } from '@ngrx/component-store'; | ||
import { concatLatestFrom } from '@ngrx/operators'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
const output = tags.stripIndent` | ||
import { ComponentStore } from '@ngrx/component-store'; | ||
import { concatLatestFrom, tapResponse } from '@ngrx/operators'; | ||
@Injectable() | ||
export class MyStore extends ComponentStore { | ||
} | ||
`; | ||
await verifySchematic(input, output); | ||
}); | ||
}); | ||
|
||
it('should add @ngrx/operators if they are missing', async () => { | ||
const originalPackageJson = JSON.parse( | ||
appTree.readContent('/package.json') | ||
); | ||
expect(originalPackageJson.dependencies['@ngrx/operators']).toBeUndefined(); | ||
expect( | ||
originalPackageJson.devDependencies['@ngrx/operators'] | ||
).toBeUndefined(); | ||
|
||
const tree = await schematicRunner.runSchematic( | ||
`ngrx-component-store-migration-18-beta`, | ||
{}, | ||
appTree | ||
); | ||
|
||
const packageJson = JSON.parse(tree.readContent('/package.json')); | ||
expect(packageJson.dependencies['@ngrx/operators']).toBeDefined(); | ||
}); | ||
}); |
157 changes: 157 additions & 0 deletions
157
modules/component-store/migrations/18_0_0-beta/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import * as ts from 'typescript'; | ||
import { | ||
Tree, | ||
Rule, | ||
chain, | ||
SchematicContext, | ||
} from '@angular-devkit/schematics'; | ||
import { | ||
addPackageToPackageJson, | ||
Change, | ||
commitChanges, | ||
createReplaceChange, | ||
InsertChange, | ||
visitTSSourceFiles, | ||
} from '../../schematics-core'; | ||
import * as os from 'os'; | ||
import { createRemoveChange } from '../../schematics-core/utility/change'; | ||
|
||
export function migrateTapResponseImport(): Rule { | ||
return (tree: Tree, ctx: SchematicContext) => { | ||
const changes: Change[] = []; | ||
addPackageToPackageJson(tree, 'dependencies', '@ngrx/operators', '^18.0.0'); | ||
|
||
visitTSSourceFiles(tree, (sourceFile) => { | ||
const importDeclarations = new Array<ts.ImportDeclaration>(); | ||
getImportDeclarations(sourceFile, importDeclarations); | ||
|
||
const componentStoreImportsAndDeclaration = importDeclarations | ||
.map((componentStoreImportDeclaration) => { | ||
const componentStoreImports = getComponentStoreNamedBinding( | ||
componentStoreImportDeclaration | ||
); | ||
if (componentStoreImports) { | ||
return { componentStoreImports, componentStoreImportDeclaration }; | ||
} else { | ||
return undefined; | ||
} | ||
}) | ||
.find(Boolean); | ||
|
||
if (!componentStoreImportsAndDeclaration) { | ||
return; | ||
} | ||
|
||
const { componentStoreImports, componentStoreImportDeclaration } = | ||
componentStoreImportsAndDeclaration; | ||
|
||
const operatorsImportDeclaration = importDeclarations.find((node) => | ||
node.moduleSpecifier.getText().includes('@ngrx/operators') | ||
); | ||
|
||
const otherComponentStoreImports = componentStoreImports.elements | ||
.filter((element) => element.name.getText() !== 'tapResponse') | ||
.map((element) => element.name.getText()) | ||
.join(', '); | ||
|
||
// Remove `tapResponse` from @ngrx/component-store and leave the other imports | ||
if (otherComponentStoreImports) { | ||
changes.push( | ||
createReplaceChange( | ||
sourceFile, | ||
componentStoreImportDeclaration, | ||
componentStoreImportDeclaration.getText(), | ||
`import { ${otherComponentStoreImports} } from '@ngrx/component-store';` | ||
) | ||
); | ||
} | ||
// Remove complete @ngrx/component-store import because it contains only `tapResponse` | ||
else { | ||
changes.push( | ||
createRemoveChange( | ||
sourceFile, | ||
componentStoreImportDeclaration, | ||
componentStoreImportDeclaration.getStart(), | ||
componentStoreImportDeclaration.getEnd() + 1 | ||
) | ||
); | ||
} | ||
|
||
let importAppendedInExistingDeclaration = false; | ||
if (operatorsImportDeclaration?.importClause?.namedBindings) { | ||
const bindings = operatorsImportDeclaration.importClause.namedBindings; | ||
if (ts.isNamedImports(bindings)) { | ||
// Add import to existing @ngrx/operators | ||
const updatedImports = [ | ||
...bindings.elements.map((element) => element.name.getText()), | ||
'tapResponse', | ||
]; | ||
const newOperatorsImport = `import { ${updatedImports.join( | ||
', ' | ||
)} } from '@ngrx/operators';`; | ||
changes.push( | ||
createReplaceChange( | ||
sourceFile, | ||
operatorsImportDeclaration, | ||
operatorsImportDeclaration.getText(), | ||
newOperatorsImport | ||
) | ||
); | ||
importAppendedInExistingDeclaration = true; | ||
} | ||
} | ||
|
||
if (!importAppendedInExistingDeclaration) { | ||
// Add new @ngrx/operators import line | ||
const newOperatorsImport = `import { tapResponse } from '@ngrx/operators';`; | ||
changes.push( | ||
new InsertChange( | ||
sourceFile.fileName, | ||
componentStoreImportDeclaration.getEnd() + 1, | ||
`${newOperatorsImport}${os.EOL}` | ||
) | ||
); | ||
} | ||
|
||
commitChanges(tree, sourceFile.fileName, changes); | ||
|
||
if (changes.length) { | ||
ctx.logger.info( | ||
`[@ngrx/component-store] Updated tapResponse to import from '@ngrx/operators'` | ||
); | ||
} | ||
}); | ||
}; | ||
} | ||
|
||
function getImportDeclarations( | ||
node: ts.Node, | ||
imports: ts.ImportDeclaration[] | ||
): void { | ||
if (ts.isImportDeclaration(node)) { | ||
imports.push(node); | ||
} | ||
|
||
ts.forEachChild(node, (childNode) => | ||
getImportDeclarations(childNode, imports) | ||
); | ||
} | ||
|
||
function getComponentStoreNamedBinding( | ||
node: ts.ImportDeclaration | ||
): ts.NamedImports | null { | ||
const namedBindings = node?.importClause?.namedBindings; | ||
if ( | ||
node.moduleSpecifier.getText().includes('@ngrx/component-store') && | ||
namedBindings && | ||
ts.isNamedImports(namedBindings) | ||
) { | ||
return namedBindings; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
export default function (): Rule { | ||
return chain([migrateTapResponseImport()]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,10 @@ | ||
{ | ||
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", | ||
"schematics": {} | ||
"schematics": { | ||
"ngrx-component-store-migration-18-beta": { | ||
"description": "As of NgRx v18, the `tapResponse` import has been removed from `@ngrx/component-store` in favor of the `@ngrx/operators` package.", | ||
"version": "18-beta", | ||
"factory": "./18_0_0-beta/index" | ||
} | ||
} | ||
} |