feat(angular): update ngrx packages to v16 (#16763)

This commit is contained in:
Leosvel Pérez Espinosa 2023-05-11 14:46:18 +01:00 committed by GitHub
parent d853ba2081
commit f2e48e429a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 539 additions and 25 deletions

View File

@ -58,9 +58,9 @@
"@nestjs/schematics": "^9.1.0",
"@nestjs/swagger": "^6.0.0",
"@nestjs/testing": "^9.0.0",
"@ngrx/effects": "~15.3.0",
"@ngrx/router-store": "~15.3.0",
"@ngrx/store": "~15.3.0",
"@ngrx/effects": "~16.0.0",
"@ngrx/router-store": "~16.0.0",
"@ngrx/store": "~16.0.0",
"@nguniversal/builders": "~16.0.0",
"@nx/cypress": "16.1.0-rc.0",
"@nx/devkit": "16.1.0-rc.0",

View File

@ -236,6 +236,15 @@
},
"description": "Update the @angular/cli package version to ~16.0.0.",
"factory": "./src/migrations/update-16-1-0/update-angular-cli"
},
"switch-data-persistence-operators-imports-to-ngrx-router-store": {
"cli": "nx",
"version": "16.2.0-beta.0",
"requires": {
"@ngrx/store": ">=16.0.0"
},
"description": "Switch the data persistence operator imports to '@ngrx/router-store/data-persistence'.",
"factory": "./src/migrations/update-16-2-0/switch-data-persistence-operators-imports-to-ngrx-router-store"
}
},
"packageJsonUpdates": {
@ -1126,6 +1135,18 @@
"alwaysAddToPackageJson": false
}
}
},
"16.2.0-ngrx": {
"version": "16.2.0-beta.0",
"requires": {
"@angular/core": "^16.0.0"
},
"packages": {
"@ngrx/store": {
"version": "~16.0.0",
"alwaysAddToPackageJson": false
}
}
}
}
}

View File

@ -0,0 +1,309 @@
import { ProjectGraph, Tree, addProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migration from './switch-data-persistence-operators-imports-to-ngrx-router-store';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => projectGraph),
}));
describe('switch-data-persistence-operators-imports-to-ngrx-router-store migration', () => {
let tree: Tree;
const file = 'apps/app1/src/app/+state/users.effects.ts';
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app1', { root: 'apps/app1' });
projectGraph = {
dependencies: {
app1: [{ source: 'app1', target: 'npm:@nx/angular', type: 'static' }],
},
nodes: {
app1: {
data: {
files: [
{
file,
hash: '',
dependencies: [
{
source: 'app1',
target: 'npm:@nx/angular',
type: 'static',
},
],
},
],
root: 'apps/app1',
},
name: 'app1',
type: 'app',
},
},
};
});
it('should do nothing when there are no imports from the angular plugin', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
@Injectable()
class UsersEffects {}
"
`);
});
it('should not replace the import path when no operator is imported', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { foo } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { foo } from '@nx/angular';
@Injectable()
class UsersEffects {}
"
`);
});
it('should not match imports from angular plugin secondary entry points', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@nx/angular/mf';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@nx/angular/mf';
@Injectable()
class UsersEffects {}
"
`);
});
it('should replace the import path in-place when it is importing an operator', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should match imports using @nrwl/angular', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@nrwl/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should support multiple operators imports', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch, navigation } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch, navigation } from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should add a separate import statement when there are operator and non-operator imports', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch, foo, navigation } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch, navigation } from '@ngrx/router-store/data-persistence';
import { foo } from '@nx/angular';
@Injectable()
class UsersEffects {}
"
`);
});
it('should support multiple import statements and import paths', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@nx/angular';
import { navigation } from '@nrwl/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch } from '@ngrx/router-store/data-persistence';
import { navigation } from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should support renamed import symbols', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch as customFetch } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch as customFetch } from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should support multiple imports with renamed and non-renamed symbols', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch as customFetch, navigation } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import {
fetch as customFetch,
navigation,
} from '@ngrx/router-store/data-persistence';
@Injectable()
class UsersEffects {}
"
`);
});
it('should add a separate import statement even with renamed symbols', async () => {
tree.write(
file,
`import { Actions, createEffect, ofType } from '@ngrx/effects';
import { fetch as customFetch, foo, navigation } from '@nx/angular';
@Injectable()
class UsersEffects {}
`
);
await migration(tree);
expect(tree.read(file, 'utf-8')).toMatchInlineSnapshot(`
"import { Actions, createEffect, ofType } from '@ngrx/effects';
import {
fetch as customFetch,
navigation,
} from '@ngrx/router-store/data-persistence';
import { foo } from '@nx/angular';
@Injectable()
class UsersEffects {}
"
`);
});
});

View File

@ -0,0 +1,184 @@
import type { FileData, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
formatFiles,
readJson,
} from '@nx/devkit';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import type { ImportDeclaration, ImportSpecifier, Node } from 'typescript';
import { FileChangeRecorder } from '../../utils/file-change-recorder';
import { ngrxVersion } from '../../utils/versions';
import { getProjectsFilteredByDependencies } from '../utils/projects';
let tsquery: typeof import('@phenomnomnominal/tsquery').tsquery;
const angularPluginTargetNames = ['npm:@nx/angular', 'npm:@nrwl/angular'];
const dataPersistenceOperators = [
'fetch',
'navigation',
'optimisticUpdate',
'pessimisticUpdate',
];
const newImportPath = '@ngrx/router-store/data-persistence';
export default async function (tree: Tree): Promise<void> {
const projects = await getProjectsFilteredByDependencies(
tree,
angularPluginTargetNames
);
if (!projects.length) {
return;
}
ensureTypescript();
tsquery = require('@phenomnomnominal/tsquery').tsquery;
const filesWithNxAngularImports: FileData[] = [];
for (const { graphNode } of projects) {
const files = filterFilesWithNxAngularDep(graphNode.data.files);
filesWithNxAngularImports.push(...files);
}
let isAnyFileUsingDataPersistence = false;
for (const { file } of filesWithNxAngularImports) {
const updated = replaceDataPersistenceInFile(tree, file);
isAnyFileUsingDataPersistence ||= updated;
}
if (isAnyFileUsingDataPersistence) {
addNgrxRouterStoreIfNotInstalled(tree);
await formatFiles(tree);
}
}
function replaceDataPersistenceInFile(tree: Tree, file: string): boolean {
const fileContents = tree.read(file, 'utf-8');
const fileAst = tsquery.ast(fileContents);
// "\\u002F" is the unicode code for "/", there's an issue with the query parser
// that prevents using "/" directly in regex queries
// https://github.com/estools/esquery/issues/68#issuecomment-415597670
const NX_ANGULAR_IMPORT_SELECTOR =
'ImportDeclaration:has(StringLiteral[value=/@(nx|nrwl)\\u002Fangular$/])';
const nxAngularImports = tsquery<ImportDeclaration>(
fileAst,
NX_ANGULAR_IMPORT_SELECTOR,
{ visitAllChildren: true }
);
if (!nxAngularImports.length) {
return false;
}
const recorder = new FileChangeRecorder(tree, file);
const IMPORT_SPECIFIERS_SELECTOR =
'ImportClause NamedImports ImportSpecifier';
for (const importDeclaration of nxAngularImports) {
const importSpecifiers = tsquery<ImportSpecifier>(
importDeclaration,
IMPORT_SPECIFIERS_SELECTOR,
{ visitAllChildren: true }
);
if (!importSpecifiers.length) {
continue;
}
// no imported symbol is a data persistence operator, skip
if (importSpecifiers.every((i) => !isOperatorImport(i))) {
continue;
}
// all imported symbols are data persistence operators, change import path
if (importSpecifiers.every((i) => isOperatorImport(i))) {
const IMPORT_PATH_SELECTOR = `${NX_ANGULAR_IMPORT_SELECTOR} > StringLiteral`;
const importPathNode = tsquery(importDeclaration, IMPORT_PATH_SELECTOR, {
visitAllChildren: true,
});
recorder.replace(importPathNode[0], `'${newImportPath}'`);
continue;
}
// mixed imports, split data persistence operators to a separate import
const operatorImportSpecifiers: string[] = [];
for (const importSpecifier of importSpecifiers) {
if (isOperatorImport(importSpecifier)) {
operatorImportSpecifiers.push(importSpecifier.getText());
recorder.remove(
importSpecifier.getStart(),
importSpecifier.getEnd() +
(hasTrailingComma(recorder.originalContent, importSpecifier)
? 1
: 0)
);
}
}
recorder.insertLeft(
importDeclaration.getStart(),
`import { ${operatorImportSpecifiers.join(
', '
)} } from '${newImportPath}';`
);
}
if (recorder.hasChanged()) {
recorder.applyChanges();
return true;
}
return false;
}
function hasTrailingComma(content: string, node: Node): boolean {
return content[node.getEnd()] === ',';
}
function isOperatorImport(importSpecifier: ImportSpecifier): boolean {
return dataPersistenceOperators.includes(
getOriginalIdentifierTextFromImportSpecifier(importSpecifier)
);
}
function getOriginalIdentifierTextFromImportSpecifier(
importSpecifier: ImportSpecifier
): string {
const children = importSpecifier.getChildren();
if (!children.length) {
return importSpecifier.getText();
}
return children[0].getText();
}
function addNgrxRouterStoreIfNotInstalled(tree: Tree): void {
const { dependencies, devDependencies } = readJson(tree, 'package.json');
if (
dependencies?.['@ngrx/router-store'] ||
devDependencies?.['@ngrx/router-store']
) {
return;
}
addDependenciesToPackageJson(tree, { '@ngrx/router-store': ngrxVersion }, {});
}
function filterFilesWithNxAngularDep(files: FileData[]): FileData[] {
const filteredFiles: FileData[] = [];
for (const file of files) {
if (
file.dependencies?.some((dep) =>
angularPluginTargetNames.includes(dep.target)
)
) {
filteredFiles.push(file);
}
}
return filteredFiles;
}

View File

@ -3,7 +3,7 @@ export const nxVersion = require('../../package.json').version;
export const angularVersion = '~16.0.0';
export const angularDevkitVersion = '~16.0.0';
export const ngPackagrVersion = '~16.0.0';
export const ngrxVersion = '~15.3.0';
export const ngrxVersion = '~16.0.0';
export const rxjsVersion = '~7.8.0';
export const zoneJsVersion = '~0.13.0';
export const angularJsVersion = '1.7.9';

42
pnpm-lock.yaml generated
View File

@ -233,14 +233,14 @@ devDependencies:
specifier: ^9.0.0
version: 9.1.6(@nestjs/common@9.1.6)(@nestjs/core@9.1.6)(@nestjs/platform-express@9.1.6)
'@ngrx/effects':
specifier: ~15.3.0
version: 15.3.0(@angular/core@16.0.0)(@ngrx/store@15.3.0)(rxjs@7.8.1)
specifier: ~16.0.0
version: 16.0.0(@angular/core@16.0.0)(@ngrx/store@16.0.0)(rxjs@7.8.1)
'@ngrx/router-store':
specifier: ~15.3.0
version: 15.3.0(@angular/common@16.0.0)(@angular/core@16.0.0)(@angular/router@16.0.0)(@ngrx/store@15.3.0)(rxjs@7.8.1)
specifier: ~16.0.0
version: 16.0.0(@angular/common@16.0.0)(@angular/core@16.0.0)(@angular/router@16.0.0)(@ngrx/store@16.0.0)(rxjs@7.8.1)
'@ngrx/store':
specifier: ~15.3.0
version: 15.3.0(@angular/core@16.0.0)(rxjs@7.8.1)
specifier: ~16.0.0
version: 16.0.0(@angular/core@16.0.0)(rxjs@7.8.1)
'@nguniversal/builders':
specifier: ~16.0.0
version: 16.0.0(@angular-devkit/build-angular@16.0.0)(@angular/common@16.0.0)(@angular/core@16.0.0)(@types/express@4.17.14)(chokidar@3.5.3)(typescript@5.0.2)
@ -4868,40 +4868,40 @@ packages:
requiresBuild: true
optional: true
/@ngrx/effects@15.3.0(@angular/core@16.0.0)(@ngrx/store@15.3.0)(rxjs@7.8.1):
resolution: {integrity: sha512-L+Ie4XFrzYBJOV7hNQvR5hUvG1PSCDd6niwOOJg5nm9zEjSnAxveJ/a3B52pRwge6EYOnrQne97jyArxOzPCJA==}
/@ngrx/effects@16.0.0(@angular/core@16.0.0)(@ngrx/store@16.0.0)(rxjs@7.8.1):
resolution: {integrity: sha512-l3H/yCwVl8DPmUasOEDthdv9lZMhCSJwBxfSXjUW7gKJVEamP3PSuvExp0ZpW9RULPblgcfTM1TH8VcPAHelQw==}
peerDependencies:
'@angular/core': ^15.0.0
'@ngrx/store': 15.3.0
'@angular/core': ^16.0.0
'@ngrx/store': 16.0.0
rxjs: ^6.5.3 || ^7.5.0
dependencies:
'@angular/core': 16.0.0(rxjs@7.8.1)(zone.js@0.13.0)
'@ngrx/store': 15.3.0(@angular/core@16.0.0)(rxjs@7.8.1)
'@ngrx/store': 16.0.0(@angular/core@16.0.0)(rxjs@7.8.1)
rxjs: 7.8.1
tslib: 2.5.0
dev: true
/@ngrx/router-store@15.3.0(@angular/common@16.0.0)(@angular/core@16.0.0)(@angular/router@16.0.0)(@ngrx/store@15.3.0)(rxjs@7.8.1):
resolution: {integrity: sha512-rAaKm6oToXF9pj/IRwsEdv/EYgQscHBfDpiAmPufFwRi/nM/NSfxtV0viZLyOw4buZi0xFwucd3aTFtaqY//vQ==}
/@ngrx/router-store@16.0.0(@angular/common@16.0.0)(@angular/core@16.0.0)(@angular/router@16.0.0)(@ngrx/store@16.0.0)(rxjs@7.8.1):
resolution: {integrity: sha512-i36reUxFSkpnEr01yZufe8H5J6Na0q/5Ul3HmT1HSG5cw0y2xIHWk2MpvCLIJjr3WeGSLvVpkQUYEdkkgmJOdw==}
peerDependencies:
'@angular/common': ^15.0.0
'@angular/core': ^15.0.0
'@angular/router': ^15.0.0
'@ngrx/store': 15.3.0
'@angular/common': ^16.0.0
'@angular/core': ^16.0.0
'@angular/router': ^16.0.0
'@ngrx/store': 16.0.0
rxjs: ^6.5.3 || ^7.5.0
dependencies:
'@angular/common': 16.0.0(@angular/core@16.0.0)(rxjs@7.8.1)
'@angular/core': 16.0.0(rxjs@7.8.1)(zone.js@0.13.0)
'@angular/router': 16.0.0(@angular/common@16.0.0)(@angular/core@16.0.0)(@angular/platform-browser@16.0.0)(rxjs@7.8.1)
'@ngrx/store': 15.3.0(@angular/core@16.0.0)(rxjs@7.8.1)
'@ngrx/store': 16.0.0(@angular/core@16.0.0)(rxjs@7.8.1)
rxjs: 7.8.1
tslib: 2.5.0
dev: true
/@ngrx/store@15.3.0(@angular/core@16.0.0)(rxjs@7.8.1):
resolution: {integrity: sha512-8cd0zWkOZ3TedDQHyOzUxZD1HHa0fU8fgzVX/2eIq6wmnleUxHVOKSJvA+DdE4GRoryFqVhAp17L1r5eC2QYHA==}
/@ngrx/store@16.0.0(@angular/core@16.0.0)(rxjs@7.8.1):
resolution: {integrity: sha512-bmr0KLITh9u1DJO51USTc4OAKX+su06efhTdNiQV/wagifpbC4kA8zr2hdstKMNG3Z5EKTX3XLFanIiREkd6JQ==}
peerDependencies:
'@angular/core': ^15.0.0
'@angular/core': ^16.0.0
rxjs: ^6.5.3 || ^7.5.0
dependencies:
'@angular/core': 16.0.0(rxjs@7.8.1)(zone.js@0.13.0)