Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(effects): add migrator for concatLatestFrom #4311

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions modules/effects/migrations/18_0_0-beta/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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('Effects 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);
});

describe('replacements', () => {
const testParams = [
{
name: 'should replace the import',
input: tags.stripIndent`
import { concatLatestFrom } from '@ngrx/effects';

@Injectable()
export class SomeEffects {

}
`,
output: tags.stripIndent`
import { concatLatestFrom } from '@ngrx/operators';

@Injectable()
export class SomeEffects {

}
`,
},
{
name: 'should also work with " as in imports',
input: tags.stripIndent`
import { concatLatestFrom } from "@ngrx/effects";

@Injectable()
export class SomeEffects {

}
`,
output: tags.stripIndent`
import { concatLatestFrom } from '@ngrx/operators';

@Injectable()
export class SomeEffects {

}
`,
},
{
name: 'should replace if multiple imports are inside an import',
input: tags.stripIndent`
import { Actions, concatLatestFrom } from '@ngrx/effects';

@Injectable()
export class SomeEffects {
actions$ = inject(Actions);

}
`,
output: tags.stripIndent`
import { Actions } from '@ngrx/effects';
import { concatLatestFrom } from '@ngrx/operators';

@Injectable()
export class SomeEffects {
actions$ = inject(Actions);

}
`,
},
];

for (const { name, input, output } of testParams) {
it(name, async () => {
appTree.create('main.ts', input);

const tree = await schematicRunner.runSchematic(
`ngrx-effects-migration-18-beta`,
{},
appTree
);

const actual = tree.readContent('main.ts');

expect(actual).toBe(output);
});
}
});

it('should add 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-effects-migration-18-beta`,
{},
appTree
);

const packageJson = JSON.parse(tree.readContent('/package.json'));
expect(packageJson.dependencies['@ngrx/operators']).toBeDefined();
});
});
112 changes: 112 additions & 0 deletions modules/effects/migrations/18_0_0-beta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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';

export function migrateConcatLatestFromImport(): Rule {
return (tree: Tree, ctx: SchematicContext) => {
const changes: Change[] = [];
addPackageToPackageJson(tree, 'dependencies', '@ngrx/operators', '^18.0.0');

visitTSSourceFiles(tree, (sourceFile) => {
visitImportDeclarations(sourceFile, (node) => {
const namedBindings = getEffectsNamedBinding(node);

if (!namedBindings) {
return;
}

const otherImports = namedBindings.elements
.filter((element) => element.name.getText() !== 'concatLatestFrom')
.map((element) => element.name.getText())
.join(', ');

namedBindings.elements.forEach((element) => {
if (element.name.getText() === 'concatLatestFrom') {
const originalImport = node.getText();
const newEffectsImport = `import { ${otherImports} } from '@ngrx/effects';`;
const newOperatorsImport = `import { concatLatestFrom } from '@ngrx/operators';`;

// Replace the original import with the new import from '@ngrx/effects'
if (otherImports) {
changes.push(
createReplaceChange(
sourceFile,
node,
originalImport,
newEffectsImport
),
new InsertChange(
sourceFile.fileName,
node.getEnd() + 1,
`${newOperatorsImport}${os.EOL}`
)
);
} else {
changes.push(
createReplaceChange(
sourceFile,
node,
node.getText(),
`import { concatLatestFrom } from '@ngrx/operators';`
)
);
}
}
});
});

commitChanges(tree, sourceFile.fileName, changes);

if (changes.length) {
ctx.logger.info(
`[@ngrx/effects] Updated concatLatestFrom to import from '@ngrx/operators'`
);
}
});
};
}

function visitImportDeclarations(
node: ts.Node,
visitor: (node: ts.ImportDeclaration) => void
) {
if (ts.isImportDeclaration(node)) {
visitor(node);
}

ts.forEachChild(node, (childNode) =>
visitImportDeclarations(childNode, visitor)
);
}

function getEffectsNamedBinding(
node: ts.ImportDeclaration
): ts.NamedImports | null {
const namedBindings = node?.importClause?.namedBindings;
if (
node.moduleSpecifier.getText().includes('@ngrx/effects') &&
namedBindings &&
ts.isNamedImports(namedBindings)
) {
return namedBindings;
}

return null;
}

export default function (): Rule {
return chain([migrateConcatLatestFromImport()]);
}
5 changes: 5 additions & 0 deletions modules/effects/migrations/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"description": "The road to v15 beta",
"version": "15-beta",
"factory": "./15_0_0-beta/index"
},
"ngrx-effects-migration-18-beta": {
"description": "As of NgRx v18, the `concatLatestFrom` import has been removed from `@ngrx/effects` in favor of the `@ngrx/operators` package.",
"version": "18-beta",
"factory": "./18_0_0-beta/index"
}
}
}