diff --git a/modules/component-store/schematics-core/tsconfig.lib.json b/modules/component-store/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/component-store/schematics-core/tsconfig.lib.json +++ b/modules/component-store/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/component-store/schematics-core/utility/visitors.ts b/modules/component-store/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/component-store/schematics-core/utility/visitors.ts +++ b/modules/component-store/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/component-store/tsconfig.schematics.json b/modules/component-store/tsconfig.schematics.json index 6ce954388f..7ae7a6a734 100644 --- a/modules/component-store/tsconfig.schematics.json +++ b/modules/component-store/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/component/schematics-core/tsconfig.lib.json b/modules/component/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/component/schematics-core/tsconfig.lib.json +++ b/modules/component/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/component/schematics-core/utility/visitors.ts b/modules/component/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/component/schematics-core/utility/visitors.ts +++ b/modules/component/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/component/tsconfig.schematics.json b/modules/component/tsconfig.schematics.json index a4ddb29020..044886ed53 100644 --- a/modules/component/tsconfig.schematics.json +++ b/modules/component/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/data/schematics-core/tsconfig.lib.json b/modules/data/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/data/schematics-core/tsconfig.lib.json +++ b/modules/data/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/data/schematics-core/utility/visitors.ts b/modules/data/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/data/schematics-core/utility/visitors.ts +++ b/modules/data/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/data/tsconfig.schematics.json b/modules/data/tsconfig.schematics.json index 10cc7e381d..f779bfe34f 100644 --- a/modules/data/tsconfig.schematics.json +++ b/modules/data/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/effects/schematics-core/tsconfig.lib.json b/modules/effects/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/effects/schematics-core/tsconfig.lib.json +++ b/modules/effects/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/effects/schematics-core/utility/visitors.ts b/modules/effects/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/effects/schematics-core/utility/visitors.ts +++ b/modules/effects/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/effects/tsconfig.schematics.json b/modules/effects/tsconfig.schematics.json index 88dcd0495a..fec5cc794a 100644 --- a/modules/effects/tsconfig.schematics.json +++ b/modules/effects/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/entity/schematics-core/tsconfig.lib.json b/modules/entity/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/entity/schematics-core/tsconfig.lib.json +++ b/modules/entity/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/entity/schematics-core/utility/visitors.ts b/modules/entity/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/entity/schematics-core/utility/visitors.ts +++ b/modules/entity/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/entity/tsconfig.schematics.json b/modules/entity/tsconfig.schematics.json index 6e4991381c..34e4210ff2 100644 --- a/modules/entity/tsconfig.schematics.json +++ b/modules/entity/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/eslint-plugin/tsconfig.schematics.json b/modules/eslint-plugin/tsconfig.schematics.json index 15bad1c7c8..96abb46af4 100644 --- a/modules/eslint-plugin/tsconfig.schematics.json +++ b/modules/eslint-plugin/tsconfig.schematics.json @@ -10,7 +10,7 @@ "outDir": "../../dist/modules/eslint-plugin", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/operators/schematics-core/tsconfig.lib.json b/modules/operators/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/operators/schematics-core/tsconfig.lib.json +++ b/modules/operators/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/operators/schematics-core/utility/visitors.ts b/modules/operators/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/operators/schematics-core/utility/visitors.ts +++ b/modules/operators/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/operators/tsconfig.schematics.json b/modules/operators/tsconfig.schematics.json index 772e1f63c6..db4d6dca3b 100644 --- a/modules/operators/tsconfig.schematics.json +++ b/modules/operators/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/router-store/schematics-core/tsconfig.lib.json b/modules/router-store/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/router-store/schematics-core/tsconfig.lib.json +++ b/modules/router-store/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/router-store/schematics-core/utility/visitors.ts b/modules/router-store/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/router-store/schematics-core/utility/visitors.ts +++ b/modules/router-store/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/router-store/tsconfig.schematics.json b/modules/router-store/tsconfig.schematics.json index b8ca15b6e3..687d09b3ae 100644 --- a/modules/router-store/tsconfig.schematics.json +++ b/modules/router-store/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/schematics-core/tsconfig.lib.json b/modules/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/schematics-core/tsconfig.lib.json +++ b/modules/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/schematics-core/utility/visitors.ts b/modules/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/schematics-core/utility/visitors.ts +++ b/modules/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/schematics/schematics-core/tsconfig.lib.json b/modules/schematics/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/schematics/schematics-core/tsconfig.lib.json +++ b/modules/schematics/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/schematics/schematics-core/utility/visitors.ts b/modules/schematics/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/schematics/schematics-core/utility/visitors.ts +++ b/modules/schematics/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/schematics/tsconfig.build.json b/modules/schematics/tsconfig.build.json index 172550d658..9727e0bca0 100644 --- a/modules/schematics/tsconfig.build.json +++ b/modules/schematics/tsconfig.build.json @@ -11,7 +11,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/schematics/tsconfig.schematics.json b/modules/schematics/tsconfig.schematics.json index 3fc1f247f3..ec9260da3e 100644 --- a/modules/schematics/tsconfig.schematics.json +++ b/modules/schematics/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/signals/migrations/18_0_0-rc_3-protected-state/index.ts b/modules/signals/migrations/18_0_0-rc_3-protected-state/index.ts index 84004d620d..9fbe95b5fb 100644 --- a/modules/signals/migrations/18_0_0-rc_3-protected-state/index.ts +++ b/modules/signals/migrations/18_0_0-rc_3-protected-state/index.ts @@ -6,6 +6,7 @@ import { import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import * as ts from 'typescript'; import { commitChanges } from '../../schematics-core'; +import { visitImportDeclaration } from '../../schematics-core/utility/visitors'; export default function migrateWritableStateSource(): Rule { return (tree: Tree, ctx: SchematicContext) => { @@ -89,19 +90,6 @@ function visitCallExpression( }); } -function visitImportDeclaration( - node: ts.Node, - callback: (importDeclaration: ts.ImportDeclaration) => void -) { - if (ts.isImportDeclaration(node)) { - callback(node); - } - - ts.forEachChild(node, (child) => { - visitImportDeclaration(child, callback); - }); -} - function findImportedName(source: ts.SourceFile) { let importedName = ''; visitImportDeclaration(source, (importDeclaration) => { diff --git a/modules/signals/migrations/19_0_0-rc_0-props/index.spec.ts b/modules/signals/migrations/19_0_0-rc_0-props/index.spec.ts new file mode 100644 index 0000000000..7822157f59 --- /dev/null +++ b/modules/signals/migrations/19_0_0-rc_0-props/index.spec.ts @@ -0,0 +1,258 @@ +import * as path from 'path'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import { createWorkspace } from '@ngrx/schematics-core/testing'; +import { tags } from '@angular-devkit/core'; + +describe('migrate to props', () => { + 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 logEntries: string[] = []; + schematicRunner.logger.subscribe((logEntry) => + logEntries.push(logEntry.message) + ); + + const tree = await schematicRunner.runSchematic( + `19_0_0-rc_0-props`, + {}, + appTree + ); + + const actual = tree.readContent('main.ts'); + + expect(actual).toBe(output); + + return logEntries; + }; + + it('renames (Named)EntityComputed imports to (Named)EntityProps', async () => { + const input = tags.stripIndent` +import { EntityComputed, NamedEntityComputed } from '@ngrx/signals/entities'; +`; + + const output = tags.stripIndent` +import { EntityProps, NamedEntityProps } from '@ngrx/signals/entities'; +`; + + const logEntries = await verifySchematic(input, output); + expect(logEntries).toEqual([ + "[@ngrx/signals] Renamed '(Named)EntityComputed' to '(Named)EntityProps' in /main.ts", + ]); + }); + + it('replaces property `computed` in `SignalStoreFeature` to `props`', async () => { + const input = tags.stripIndent` +import { signalStoreFeature, type } from '@ngrx/signals'; +import { Signal } from '@angular/core'; + +export function withMyFeature() { + return signalStoreFeature({ computed: type<{ num: Signal }>() }); +} +`; + + const output = tags.stripIndent` +import { signalStoreFeature, type } from '@ngrx/signals'; +import { Signal } from '@angular/core'; + +export function withMyFeature() { + return signalStoreFeature({ props: type<{ num: Signal }>() }); +} +`; + + const logEntries = await verifySchematic(input, output); + expect(logEntries).toEqual([ + "[@ngrx/signals] Renamed 'computed' to 'props' in signalStoreFeature() in /main.ts", + ]); + }); + + it('replaces property `computed` in `type` with `signalStoreFeature` to `props`', async () => { + const input = tags.stripIndent` +export function withMyFeature() { + return signalStoreFeature( + type<{ computed: { num: Signal } }>() + ); +} +`; + + const output = tags.stripIndent` +export function withMyFeature() { + return signalStoreFeature( + type<{ props: { num: Signal } }>() + ); +} +`; + + await verifySchematic(input, output); + }); + + it('replaces `computed` in `SignalStoreFeature` to `props`', async () => { + const input = tags.stripIndent` +import { SignalStoreFeature } from '@ngrx/signals'; +import { Signal } from '@angular/core'; + +declare function withMyFeature(): SignalStoreFeature< + { state: {}; computed: { num1: Signal }; methods: {} }, + { state: {}; computed: { num2: Signal }; methods: {} } +>; +`; + + const output = tags.stripIndent` +import { SignalStoreFeature } from '@ngrx/signals'; +import { Signal } from '@angular/core'; + +declare function withMyFeature(): SignalStoreFeature< + { state: {}; props: { num1: Signal }; methods: {} }, + { state: {}; props: { num2: Signal }; methods: {} } +>; +`; + + const logEntries = await verifySchematic(input, output); + expect(logEntries).toEqual([ + "[@ngrx/signals] Renamed 'computed' to 'props' in SignalStoreFeature<> in /main.ts", + ]); + }); + + test('kitchen sink', async () => { + const input = tags.stripIndent` +import { EntityComputed, NamedEntityComputed } from '@ngrx/signals/entities'; +import { + EmptyFeatureResult, + SignalStoreFeature, + signalStoreFeature, + type, + withHooks, + withMethods, +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tap } from 'rxjs/operators'; +import { Signal } from '@angular/core'; + +declare function withNamedDataService< + E extends { id: number }, + Collection extends string +>(): SignalStoreFeature< + EmptyFeatureResult & { computed: NamedEntityComputed } +>; + +declare function withDataService< + E extends { id: number }, + Collection extends string +>(): SignalStoreFeature }>; + +export function withConsoleLogger() { + return signalStoreFeature( + { computed: type<{ pretty: Signal }>() }, + withMethods(() => ({ + log: rxMethod(tap((message) => console.log(message))), + })), + withHooks((store) => ({ + onInit() { + store.log(store.pretty()); + }, + })) + ); +}`; + + const output = tags.stripIndent` +import { EntityProps, NamedEntityProps } from '@ngrx/signals/entities'; +import { + EmptyFeatureResult, + SignalStoreFeature, + signalStoreFeature, + type, + withHooks, + withMethods, +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tap } from 'rxjs/operators'; +import { Signal } from '@angular/core'; + +declare function withNamedDataService< + E extends { id: number }, + Collection extends string +>(): SignalStoreFeature< + EmptyFeatureResult & { props: NamedEntityProps } +>; + +declare function withDataService< + E extends { id: number }, + Collection extends string +>(): SignalStoreFeature }>; + +export function withConsoleLogger() { + return signalStoreFeature( + { props: type<{ pretty: Signal }>() }, + withMethods(() => ({ + log: rxMethod(tap((message) => console.log(message))), + })), + withHooks((store) => ({ + onInit() { + store.log(store.pretty()); + }, + })) + ); +}`; + + const logEntries = await verifySchematic(input, output); + expect(logEntries).toEqual([ + "[@ngrx/signals] Renamed '(Named)EntityComputed' to '(Named)EntityProps' in /main.ts", + "[@ngrx/signals] Renamed 'computed' to 'props' in SignalStoreFeature<> in /main.ts", + "[@ngrx/signals] Renamed 'computed' to 'props' in signalStoreFeature() in /main.ts", + ]); + }); + + it('creates two files with minimal changes and checks both files', async () => { + const input1 = tags.stripIndent` +import { EntityComputed } from '@ngrx/signals/entities'; +`; + + const output1 = tags.stripIndent` +import { EntityProps } from '@ngrx/signals/entities'; +`; + + const input2 = tags.stripIndent` +import { NamedEntityComputed } from '@ngrx/signals/entities'; +`; + + const output2 = tags.stripIndent` +import { NamedEntityProps } from '@ngrx/signals/entities'; + `; + + appTree.create('file1.ts', input1); + appTree.create('file2.ts', input2); + + const logEntries: string[] = []; + schematicRunner.logger.subscribe((logEntry) => + logEntries.push(logEntry.message) + ); + + const tree = await schematicRunner.runSchematic( + `19_0_0-rc_0-props`, + {}, + appTree + ); + + const actual1 = tree.readContent('file1.ts'); + const actual2 = tree.readContent('file2.ts'); + + expect(actual1).toBe(output1); + expect(actual2).toBe(output2); + + expect(logEntries).toEqual([ + "[@ngrx/signals] Renamed '(Named)EntityComputed' to '(Named)EntityProps' in /file1.ts", + "[@ngrx/signals] Renamed '(Named)EntityComputed' to '(Named)EntityProps' in /file2.ts", + ]); + }); +}); diff --git a/modules/signals/migrations/19_0_0-rc_0-props/index.ts b/modules/signals/migrations/19_0_0-rc_0-props/index.ts new file mode 100644 index 0000000000..919ae85230 --- /dev/null +++ b/modules/signals/migrations/19_0_0-rc_0-props/index.ts @@ -0,0 +1,224 @@ +import { + chain, + Rule, + SchematicContext, + Tree, +} from '@angular-devkit/schematics'; +import { + Change, + commitChanges, + createReplaceChange, + visitTSSourceFiles, +} from '../../schematics-core'; +import { + visitCallExpression, + visitImportDeclaration, + visitImportSpecifier, + visitTypeLiteral, + visitTypeReference, +} from '../../schematics-core/utility/visitors'; +import * as ts from 'typescript'; + +function migratedToEntityProps(sourceFile: ts.SourceFile) { + const changes: Change[] = []; + visitImportDeclaration(sourceFile, (importDeclaration, moduleName) => { + if (moduleName !== '@ngrx/signals/entities') { + return; + } + + visitImportSpecifier(importDeclaration, (importSpecifier) => { + if (importSpecifier.name.getText() === 'EntityComputed') { + changes.push( + createReplaceChange( + sourceFile, + importSpecifier, + importSpecifier.getText(), + 'EntityProps' + ) + ); + + visitTypeReference(sourceFile, (type) => { + if (type.typeName.getText() === 'EntityComputed') { + changes.push( + createReplaceChange( + sourceFile, + type, + type.typeName.getText(), + 'EntityProps' + ) + ); + } + }); + } + + if (importSpecifier.name.getText() === 'NamedEntityComputed') { + changes.push( + createReplaceChange( + sourceFile, + importSpecifier, + importSpecifier.getText(), + 'NamedEntityProps' + ) + ); + + visitTypeReference(sourceFile, (typeReference) => { + if (typeReference.typeName.getText() === 'NamedEntityComputed') { + changes.push( + createReplaceChange( + sourceFile, + typeReference.typeName, + typeReference.typeName.getText(), + 'NamedEntityProps' + ) + ); + } + }); + } + }); + }); + + return changes; +} + +function migrateToPropsInSignalStoreFeatureType( + sourceFile: ts.SourceFile +): Change[] { + const changes: Change[] = []; + visitTypeReference(sourceFile, (typeReference) => { + if (typeReference.typeName.getText() !== 'SignalStoreFeature') { + return; + } + + visitTypeLiteral(typeReference, (typeLiteral) => { + const typeLiteralChildren = typeLiteral.members; + for (const propertySignature of typeLiteralChildren) { + if (ts.isPropertySignature(propertySignature)) { + if (propertySignature.name.getText() === 'computed') { + changes.push( + createReplaceChange( + sourceFile, + propertySignature.name, + 'computed', + 'props' + ) + ); + } + } + } + }); + }); + + return changes; +} + +function migrateToPropsInSignalStoreFeatureWithObjectLiteral( + objectLiteral: ts.ObjectLiteralExpression, + sourceFile: ts.SourceFile +): Change[] { + const computedKey = objectLiteral.properties + .filter(ts.isPropertyAssignment) + .find((property) => property.name.getText() === 'computed'); + if (computedKey) { + return [createReplaceChange(sourceFile, computedKey, 'computed', 'props')]; + } + + return []; +} + +function migrateToPropsInSignalStoreFeatureWithCallExpression( + callExpression: ts.CallExpression, + sourceFile: ts.SourceFile +): Change[] { + if (callExpression.expression.getText() === 'type') { + const typeArgument = callExpression.typeArguments?.at(0); + + if (typeArgument && ts.isTypeLiteralNode(typeArgument)) { + const computedKey = typeArgument.members + .filter(ts.isPropertySignature) + .find( + (propertySignature) => propertySignature.name.getText() === 'computed' + ); + + if (computedKey) { + return [ + createReplaceChange(sourceFile, computedKey, 'computed', 'props'), + ]; + } + } + } + + return []; +} + +function migrateToPropsInSignalStoreFeatureFunction( + sourceFile: ts.SourceFile +): Change[] { + const changes: Change[] = []; + visitCallExpression(sourceFile, (callExpression) => { + if (callExpression.expression.getText() !== 'signalStoreFeature') { + return; + } + + const objectLiteralOrCallExpression = callExpression.arguments[0]; + if (!objectLiteralOrCallExpression) { + return; + } + + if (ts.isObjectLiteralExpression(objectLiteralOrCallExpression)) { + changes.push( + ...migrateToPropsInSignalStoreFeatureWithObjectLiteral( + objectLiteralOrCallExpression, + sourceFile + ) + ); + } else if (ts.isCallExpression(objectLiteralOrCallExpression)) { + changes.push( + ...migrateToPropsInSignalStoreFeatureWithCallExpression( + objectLiteralOrCallExpression, + sourceFile + ) + ); + } + }); + + return changes; +} + +export function migrate(): Rule { + return (tree: Tree, ctx: SchematicContext) => { + visitTSSourceFiles(tree, (sourceFile) => { + const entityPropsChanges = migratedToEntityProps(sourceFile); + const propsInSignalStoreFeatureTypeChanges = + migrateToPropsInSignalStoreFeatureType(sourceFile); + const propsInSignalStoreFeatureFunctionChanges = + migrateToPropsInSignalStoreFeatureFunction(sourceFile); + const changes = [ + ...entityPropsChanges, + ...propsInSignalStoreFeatureTypeChanges, + ...propsInSignalStoreFeatureFunctionChanges, + ]; + + commitChanges(tree, sourceFile.fileName, changes); + + if (entityPropsChanges.length) { + ctx.logger.info( + `[@ngrx/signals] Renamed '(Named)EntityComputed' to '(Named)EntityProps' in ${sourceFile.fileName}` + ); + } + if (propsInSignalStoreFeatureTypeChanges.length) { + ctx.logger.info( + `[@ngrx/signals] Renamed 'computed' to 'props' in SignalStoreFeature<> in ${sourceFile.fileName}` + ); + } + if (propsInSignalStoreFeatureFunctionChanges.length) { + ctx.logger.info( + `[@ngrx/signals] Renamed 'computed' to 'props' in signalStoreFeature() in ${sourceFile.fileName}` + ); + } + }); + }; +} + +export default function (): Rule { + return chain([migrate()]); +} diff --git a/modules/signals/migrations/migration.json b/modules/signals/migrations/migration.json index 9d2168ca35..d208435769 100644 --- a/modules/signals/migrations/migration.json +++ b/modules/signals/migrations/migration.json @@ -10,6 +10,11 @@ "description": "Replace StateSignal usages with WritableStateSource", "version": "18.0.0-rc.3", "factory": "./18_0_0-rc_3-writablestatesource/index" + }, + "19_0_0-rc_0-props": { + "description": "Replace several properties with a single props object", + "version": "19.0.0-rc.0", + "factory": "./19_0_0-rc_0-props/index" } } } diff --git a/modules/signals/schematics-core/tsconfig.lib.json b/modules/signals/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/signals/schematics-core/tsconfig.lib.json +++ b/modules/signals/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/signals/schematics-core/utility/visitors.ts b/modules/signals/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/signals/schematics-core/utility/visitors.ts +++ b/modules/signals/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/signals/spec/deep-signal.spec.ts b/modules/signals/spec/deep-signal.spec.ts new file mode 100644 index 0000000000..0fdd89795e --- /dev/null +++ b/modules/signals/spec/deep-signal.spec.ts @@ -0,0 +1,161 @@ +import { isSignal, signal } from '@angular/core'; +import { toDeepSignal } from '../src/deep-signal'; + +describe('toDeepSignal', () => { + it('creates deep signals for plain objects', () => { + const sig = signal({ m: { s: 't' } }); + const deepSig = toDeepSignal(sig); + + expect(sig).not.toBe(deepSig); + + expect(isSignal(deepSig)).toBe(true); + expect(deepSig()).toEqual({ m: { s: 't' } }); + + expect(isSignal(deepSig.m)).toBe(true); + expect(deepSig.m()).toEqual({ s: 't' }); + + expect(isSignal(deepSig.m.s)).toBe(true); + expect(deepSig.m.s()).toBe('t'); + }); + + it('creates deep signals for custom class instances', () => { + class User { + constructor(readonly firstName: string) {} + } + + class UserState { + constructor(readonly user: User) {} + } + + const sig = signal(new UserState(new User('John'))); + const deepSig = toDeepSignal(sig); + + expect(sig).not.toBe(deepSig); + + expect(isSignal(deepSig)).toBe(true); + expect(deepSig()).toEqual({ user: { firstName: 'John' } }); + + expect(isSignal(deepSig.user)).toBe(true); + expect(deepSig.user()).toEqual({ firstName: 'John' }); + + expect(isSignal(deepSig.user.firstName)).toBe(true); + expect(deepSig.user.firstName()).toBe('John'); + }); + + it('does not create deep signals for primitives', () => { + const num = signal(0); + const str = signal('str'); + const bool = signal(true); + + const deepNum = toDeepSignal(num); + const deepStr = toDeepSignal(str); + const deepBool = toDeepSignal(bool); + + expect(deepNum).toBe(num); + expect(deepStr).toBe(str); + expect(deepBool).toBe(bool); + }); + + it('does not create deep signals for iterables', () => { + const array = signal([]); + const set = signal(new Set()); + const map = signal(new Map()); + const uintArray = signal(new Uint32Array()); + const floatArray = signal(new Float64Array()); + + const deepArray = toDeepSignal(array); + const deepSet = toDeepSignal(set); + const deepMap = toDeepSignal(map); + const deepUintArray = toDeepSignal(uintArray); + const deepFloatArray = toDeepSignal(floatArray); + + expect(deepArray).toBe(array); + expect(deepSet).toBe(set); + expect(deepMap).toBe(map); + expect(deepUintArray).toBe(uintArray); + expect(deepFloatArray).toBe(floatArray); + }); + + it('does not create deep signals for built-in object types', () => { + const weakSet = signal(new WeakSet()); + const weakMap = signal(new WeakMap()); + const promise = signal(Promise.resolve(10)); + const date = signal(new Date()); + const error = signal(new Error()); + const regExp = signal(new RegExp('')); + const arrayBuffer = signal(new ArrayBuffer(10)); + const dataView = signal(new DataView(new ArrayBuffer(10))); + + const deepWeakSet = toDeepSignal(weakSet); + const deepWeakMap = toDeepSignal(weakMap); + const deepPromise = toDeepSignal(promise); + const deepDate = toDeepSignal(date); + const deepError = toDeepSignal(error); + const deepRegExp = toDeepSignal(regExp); + const deepArrayBuffer = toDeepSignal(arrayBuffer); + const deepDataView = toDeepSignal(dataView); + + expect(deepWeakSet).toBe(weakSet); + expect(deepWeakMap).toBe(weakMap); + expect(deepPromise).toBe(promise); + expect(deepDate).toBe(date); + expect(deepError).toBe(error); + expect(deepRegExp).toBe(regExp); + expect(deepArrayBuffer).toBe(arrayBuffer); + expect(deepDataView).toBe(dataView); + }); + + it('does not create deep signals for functions', () => { + const fn1 = signal(new Function()); + const fn2 = signal(function () {}); + const fn3 = signal(() => {}); + + const deepFn1 = toDeepSignal(fn1); + const deepFn2 = toDeepSignal(fn2); + const deepFn3 = toDeepSignal(fn3); + + expect(deepFn1).toBe(fn1); + expect(deepFn2).toBe(fn2); + expect(deepFn3).toBe(fn3); + }); + + it('does not create deep signals for custom class instances that are iterables', () => { + class CustomArray extends Array {} + + class CustomSet extends Set {} + + class CustomFloatArray extends Float32Array {} + + const array = signal(new CustomArray()); + const floatArray = signal(new CustomFloatArray()); + const set = signal(new CustomSet()); + + const deepArray = toDeepSignal(array); + const deepFloatArray = toDeepSignal(floatArray); + const deepSet = toDeepSignal(set); + + expect(deepArray).toBe(array); + expect(deepFloatArray).toBe(floatArray); + expect(deepSet).toBe(set); + }); + + it('does not create deep signals for custom class instances that extend built-in object types', () => { + class CustomWeakMap extends WeakMap {} + + class CustomError extends Error {} + + class CustomArrayBuffer extends ArrayBuffer {} + + const weakMap = signal(new CustomWeakMap()); + const error = signal(new CustomError()); + const arrayBuffer = signal(new CustomArrayBuffer(10)); + + const deepWeakMap = toDeepSignal(weakMap); + const deepError = toDeepSignal(error); + const deepArrayBuffer = toDeepSignal(arrayBuffer); + + expect(deepWeakMap).toBe(weakMap); + expect(deepError).toBe(error); + expect(deepArrayBuffer).toBe(arrayBuffer); + }); +}); diff --git a/modules/signals/spec/types/signal-state.types.spec.ts b/modules/signals/spec/types/signal-state.types.spec.ts index c9edeab141..e4bf63b2ad 100644 --- a/modules/signals/spec/types/signal-state.types.spec.ts +++ b/modules/signals/spec/types/signal-state.types.spec.ts @@ -118,37 +118,85 @@ describe('signalState', () => { expectSnippet(snippet).toInfer('set', 'Signal>'); }); - it('does not create deep signals for an array', () => { + it('does not create deep signals for iterables', () => { const snippet = ` - const state = signalState([]); - declare const stateKeys: keyof typeof state; + const arrayState = signalState([]); + declare const arrayStateKeys: keyof typeof arrayState; + + const setState = signalState(new Set()); + declare const setStateKeys: keyof typeof setState; + + const mapState = signalState(new Map()); + declare const mapStateKeys: keyof typeof mapState; + + const uintArrayState = signalState(new Uint8ClampedArray()); + declare const uintArrayStateKeys: keyof typeof uintArrayState; `; expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', + 'arrayStateKeys', 'unique symbol | keyof Signal' ); + + expectSnippet(snippet).toInfer( + 'setStateKeys', + 'unique symbol | keyof Signal>' + ); + + expectSnippet(snippet).toInfer( + 'mapStateKeys', + 'unique symbol | keyof Signal>' + ); + + expectSnippet(snippet).toInfer( + 'uintArrayStateKeys', + 'unique symbol | keyof Signal' + ); }); - it('does not create deep signals for Map', () => { + it('does not create deep signals for built-in object types', () => { const snippet = ` - const state = signalState(new Map()); - declare const stateKeys: keyof typeof state; + const weakSetState = signalState(new WeakSet<{ foo: string }>()); + declare const weakSetStateKeys: keyof typeof weakSetState; + + const dateState = signalState(new Date()); + declare const dateStateKeys: keyof typeof dateState; + + const errorState = signalState(new Error()); + declare const errorStateKeys: keyof typeof errorState; + + const regExpState = signalState(new RegExp('')); + declare const regExpStateKeys: keyof typeof regExpState; `; expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', - 'unique symbol | keyof Signal>' + 'weakSetStateKeys', + 'unique symbol | keyof Signal>' + ); + + expectSnippet(snippet).toInfer( + 'dateStateKeys', + 'unique symbol | keyof Signal' + ); + + expectSnippet(snippet).toInfer( + 'errorStateKeys', + 'unique symbol | keyof Signal' + ); + + expectSnippet(snippet).toInfer( + 'regExpStateKeys', + 'unique symbol | keyof Signal' ); }); - it('does not create deep signals for Set', () => { + it('does not create deep signals for functions', () => { const snippet = ` - const state = signalState(new Set()); + const state = signalState(() => {}); declare const stateKeys: keyof typeof state; `; @@ -156,7 +204,7 @@ describe('signalState', () => { expectSnippet(snippet).toInfer( 'stateKeys', - 'unique symbol | keyof Signal>' + 'unique symbol | keyof Signal<() => void>' ); }); diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index aeb0ab73f4..6854f27064 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -163,33 +163,63 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer('set', 'Signal>'); }); - it('does not create deep signals when state type is an array', () => { + it('does not create deep signals when state type is an iterable', () => { const snippet = ` - const Store = signalStore(withState([])); - const store = new Store(); - declare const storeKeys: keyof typeof store; + const ArrayStore = signalStore(withState([])); + const arrayStore = new ArrayStore(); + declare const arrayStoreKeys: keyof typeof arrayStore; + + const SetStore = signalStore(withState(new Set<{ foo: string }>())); + const setStore = new SetStore(); + declare const setStoreKeys: keyof typeof setStore; + + const MapStore = signalStore(withState(new Map())); + const mapStore = new MapStore(); + declare const mapStoreKeys: keyof typeof mapStore; + + const FloatArrayStore = signalStore(withState(new Float32Array())); + const floatArrayStore = new FloatArrayStore(); + declare const floatArrayStoreKeys: keyof typeof floatArrayStore; `; expectSnippet(snippet).toSucceed(); - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('arrayStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('setStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('mapStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('floatArrayStoreKeys', 'unique symbol'); }); - it('does not create deep signals when state type is Map', () => { + it('does not create deep signals when state type is a built-in object type', () => { const snippet = ` - const Store = signalStore(withState(new Map())); - const store = new Store(); - declare const storeKeys: keyof typeof store; + const WeakMapStore = signalStore(withState(new WeakMap<{ foo: string }, { bar: number }>())); + const weakMapStore = new WeakMapStore(); + declare const weakMapStoreKeys: keyof typeof weakMapStore; + + const DateStore = signalStore(withState(new Date())); + const dateStore = new DateStore(); + declare const dateStoreKeys: keyof typeof dateStore; + + const ErrorStore = signalStore(withState(new Error())); + const errorStore = new ErrorStore(); + declare const errorStoreKeys: keyof typeof errorStore; + + const RegExpStore = signalStore(withState(new RegExp(''))); + const regExpStore = new RegExpStore(); + declare const regExpStoreKeys: keyof typeof regExpStore; `; expectSnippet(snippet).toSucceed(); - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('weakMapStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('dateStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('errorStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('regExpStoreKeys', 'unique symbol'); }); - it('does not create deep signals when state type is Set', () => { + it('does not create deep signals when state type is a function', () => { const snippet = ` - const Store = signalStore(withState(new Set<{ foo: string }>())); + const Store = signalStore(withState(() => () => {})); const store = new Store(); declare const storeKeys: keyof typeof store; `; diff --git a/modules/signals/src/deep-signal.ts b/modules/signals/src/deep-signal.ts index 733e085fc7..7610d9ad8e 100644 --- a/modules/signals/src/deep-signal.ts +++ b/modules/signals/src/deep-signal.ts @@ -46,6 +46,38 @@ export function toDeepSignal(signal: Signal): DeepSignal { }); } +const nonRecords = [ + WeakSet, + WeakMap, + Promise, + Date, + Error, + RegExp, + ArrayBuffer, + DataView, + Function, +]; + function isRecord(value: unknown): value is Record { - return value?.constructor === Object; + if (value === null || typeof value !== 'object' || isIterable(value)) { + return false; + } + + let proto = Object.getPrototypeOf(value); + if (proto === Object.prototype) { + return true; + } + + while (proto && proto !== Object.prototype) { + if (nonRecords.includes(proto.constructor)) { + return false; + } + proto = Object.getPrototypeOf(proto); + } + + return proto === Object.prototype; +} + +function isIterable(value: any): value is Iterable { + return typeof value?.[Symbol.iterator] === 'function'; } diff --git a/modules/signals/src/ts-helpers.ts b/modules/signals/src/ts-helpers.ts index f7df5cdb77..bf94d5965b 100644 --- a/modules/signals/src/ts-helpers.ts +++ b/modules/signals/src/ts-helpers.ts @@ -1,13 +1,19 @@ +type NonRecord = + | Iterable + | WeakSet + | WeakMap + | Promise + | Date + | Error + | RegExp + | ArrayBuffer + | DataView + | Function; + export type Prettify = { [K in keyof T]: T[K] } & {}; export type IsRecord = T extends object - ? T extends unknown[] - ? false - : T extends Set - ? false - : T extends Map - ? false - : T extends Function + ? T extends NonRecord ? false : true : false; diff --git a/modules/signals/tsconfig.schematics.json b/modules/signals/tsconfig.schematics.json index c5bb5a37e5..66bc9d8890 100644 --- a/modules/signals/tsconfig.schematics.json +++ b/modules/signals/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/store-devtools/schematics-core/tsconfig.lib.json b/modules/store-devtools/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/store-devtools/schematics-core/tsconfig.lib.json +++ b/modules/store-devtools/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/store-devtools/schematics-core/utility/visitors.ts b/modules/store-devtools/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/store-devtools/schematics-core/utility/visitors.ts +++ b/modules/store-devtools/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/store-devtools/tsconfig.schematics.json b/modules/store-devtools/tsconfig.schematics.json index 2dfddef2e7..ebb3b29630 100644 --- a/modules/store-devtools/tsconfig.schematics.json +++ b/modules/store-devtools/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/store/schematics-core/tsconfig.lib.json b/modules/store/schematics-core/tsconfig.lib.json index c0f1097a2b..13021f14d8 100644 --- a/modules/store/schematics-core/tsconfig.lib.json +++ b/modules/store/schematics-core/tsconfig.lib.json @@ -8,7 +8,7 @@ "outDir": "../../dist/modules/schematics-score", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/modules/store/schematics-core/utility/visitors.ts b/modules/store/schematics-core/utility/visitors.ts index fa4edd06af..b0e0229e13 100644 --- a/modules/store/schematics-core/utility/visitors.ts +++ b/modules/store/schematics-core/utility/visitors.ts @@ -198,6 +198,86 @@ export function visitDecorator( }); } +export function visitImportDeclaration( + node: ts.Node, + callback: ( + importDeclaration: ts.ImportDeclaration, + moduleName?: string + ) => void +) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier.getText(); + const moduleName = moduleSpecifier.replaceAll('"', '').replaceAll("'", ''); + + callback(node, moduleName); + } + + ts.forEachChild(node, (child) => { + visitImportDeclaration(child, callback); + }); +} + +export function visitImportSpecifier( + node: ts.ImportDeclaration, + callback: (importSpecifier: ts.ImportSpecifier) => void +) { + const { importClause } = node; + if (!importClause) { + return; + } + + const importClauseChildren = importClause.getChildren(); + for (const namedImport of importClauseChildren) { + if (ts.isNamedImports(namedImport)) { + const namedImportChildren = namedImport.elements; + for (const importSpecifier of namedImportChildren) { + if (ts.isImportSpecifier(importSpecifier)) { + callback(importSpecifier); + } + } + } + } +} + +export function visitTypeReference( + node: ts.Node, + callback: (typeReference: ts.TypeReferenceNode) => void +) { + if (ts.isTypeReferenceNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeReference(child, callback); + }); +} + +export function visitTypeLiteral( + node: ts.Node, + callback: (typeLiteral: ts.TypeLiteralNode) => void +) { + if (ts.isTypeLiteralNode(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitTypeLiteral(child, callback); + }); +} + +export function visitCallExpression( + node: ts.Node, + callback: (callExpression: ts.CallExpression) => void +) { + if (ts.isCallExpression(node)) { + callback(node); + } + + ts.forEachChild(node, (child) => { + visitCallExpression(child, callback); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { diff --git a/modules/store/tsconfig.schematics.json b/modules/store/tsconfig.schematics.json index e53393e33e..27b53d323c 100644 --- a/modules/store/tsconfig.schematics.json +++ b/modules/store/tsconfig.schematics.json @@ -12,7 +12,7 @@ }, "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["ES2022", "dom"], "skipLibCheck": true, "strict": true }, diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 83ff0ab51c..d8b883c96a 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -24,20 +24,25 @@ In a service-based application, your components interact with data through many Imagine that your application manages movies. Here is a component that fetches and displays a list of movies. +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; + @Component({ template: ` <li *ngFor="let movie of movies"> {{ movie.name }} </li> - ` + `, + standalone: true, + imports: [CommonModule], }) export class MoviesPageComponent { - movies: Movie[]; - - constructor(private movieService: MoviesService) {} + private moviesService = inject(MoviesService); + protected movies: Movie[]; ngOnInit() { - this.movieService.getAll().subscribe(movies => this.movies = movies); + this.movieService.getAll() + .subscribe(movies => this.movies = movies); } } @@ -45,14 +50,18 @@ export class MoviesPageComponent { You also have the corresponding service that handles the fetching of movies. +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MoviesService { - constructor (private http: HttpClient) {} + private http = inject(HttpClient); - getAll() { - return this.http.get('/movies'); + getAll(): Observable { + return this.http.get('/movies'); } } @@ -68,17 +77,21 @@ The component has multiple responsibilities: Effects handle external data and interactions, allowing your services to be less stateful and only perform tasks related to external interactions. Next, refactor the component to put the shared movie data in the `Store`. Effects handle the fetching of movie data. +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; + @Component({ template: ` <div *ngFor="let movie of movies$ | async"> {{ movie.name }} </div> - ` + `, + standalone: true, + imports: [CommonModule], }) -export class MoviesPageComponent { - movies$: Observable<Movie[]> = this.store.select(state => state.movies); - - constructor(private store: Store<{ movies: Movie[] }>) {} +export class MoviesPageComponent implements OnInit { + private store = inject(Store<{ movies: Movie[] }>); + protected movies$ = this.store.select(state => state.movies); ngOnInit() { this.store.dispatch({ type: '[Movies Page] Load Movies' }); @@ -109,7 +122,7 @@ Effects are injectable service classes with distinct parts: To show how you handle loading movies from the example above, let's look at `MoviesEffects`. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { EMPTY } from 'rxjs'; import { map, exhaustMap, catchError } from 'rxjs/operators'; @@ -117,21 +130,19 @@ import { MoviesService } from './movies.service'; @Injectable() export class MoviesEffects { - - loadMovies$ = createEffect(() => this.actions$.pipe( - ofType('[Movies Page] Load Movies'), - exhaustMap(() => this.moviesService.getAll() - .pipe( - map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })), - catchError(() => EMPTY) - )) - ) - ); - - constructor( - private actions$: Actions, - private moviesService: MoviesService - ) {} + private actions$ = inject(Actions); + private moviesService = inject(MoviesService); + + loadMovies$ = createEffect(() => { + return this.actions$.pipe( + ofType('[Movies Page] Load Movies'), + exhaustMap(() => this.moviesService.getAll() + .pipe( + map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })), + catchError(() => EMPTY) + )) + ); + }); } @@ -145,10 +156,10 @@ The `loadMovies$` effect is listening for all dispatched actions through the `Ac ## Handling Errors -Effects are built on top of observable streams provided by RxJS. Effects are listeners of observable streams that continue until an error or completion occurs. In order for effects to continue running in the event of an error in the observable, or completion of the observable stream, they must be nested within a "flattening" operator, such as `mergeMap`, `concatMap`, `exhaustMap` and other flattening operators. The example below shows the `loadMovies$` effect handling errors when fetching movies. +Effects are built on top of observable streams provided by RxJS. Effects are listeners of observable streams that continue until an error or completion occurs. In order for effects to continue running in the event of an error in the observable, or completion of the observable stream, they must be nested within a "flattening" operator, such as `mergeMap`, `concatMap`, `exhaustMap`, and `switchMap`. The example below shows the `loadMovies$` effect handling errors when fetching movies. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { map, exhaustMap, catchError } from 'rxjs/operators'; @@ -156,9 +167,11 @@ import { MoviesService } from './movies.service'; @Injectable() export class MoviesEffects { + private actions$ = inject(Actions); + private moviesService = inject(MoviesService); - loadMovies$ = createEffect(() => - this.actions$.pipe( + loadMovies$ = createEffect(() => { + return this.actions$.pipe( ofType('[Movies Page] Load Movies'), exhaustMap(() => this.moviesService.getAll() .pipe( @@ -166,13 +179,8 @@ export class MoviesEffects { catchError(() => of({ type: '[Movies API] Movies Loaded Error' })) ) ) - ) - ); - - constructor( - private actions$: Actions, - private moviesService: MoviesService - ) {} + ); + }); } @@ -227,35 +235,19 @@ It's recommended to inject all dependencies as effect function arguments for eas -## Registering Root Effects - -After you've written class-based or functional effects, you must register them so the effects start running. To register root-level effects, add the `EffectsModule.forRoot()` method with an array or sequence of effects classes and/or functional effects dictionaries to your `AppModule`. +## Registering Effects - -import { NgModule } from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; +Effect classes and functional effects are registered using the `provideEffects` method. -@NgModule({ - imports: [ - EffectsModule.forRoot(MoviesEffects, actorsEffects), - ], -}) -export class AppModule {} - +At the root level, effects are registered in the `providers` array of the application configuration. -
+
-The `EffectsModule.forRoot()` method must be added to your `AppModule` imports even if you don't register any root-level effects. +Effects start running **immediately** after instantiation to ensure they are listening for all relevant actions as soon as possible. +Services used in root-level effects are **not** recommended to be used with services that are used with the `APP_INITIALIZER` token.
-### Using the Standalone API - -Registering effects can also be done using the standalone APIs if you are bootstrapping an Angular application using standalone features. - import { bootstrapApplication } from '@angular/platform-browser'; import { provideStore } from '@ngrx/store'; @@ -273,35 +265,15 @@ bootstrapApplication(AppComponent, { }); +Feature-level effects are registered in the `providers` array of the route config. +The same `provideEffects()` method is used to register effects for a feature. +
-Effects start running **immediately** after instantiation to ensure they are listening for all relevant actions as soon as possible. Services used in root-level effects are **not** recommended to be used with services that are used with the `APP_INITIALIZER` token. +Registering an effects class multiple times (for example in different lazy loaded features) does not cause the effects to run multiple times.
-## Registering Feature Effects - -For feature modules, register your effects by adding the `EffectsModule.forFeature()` method in the `imports` array of your `NgModule`. - - -import { NgModule } from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -@NgModule({ - imports: [ - EffectsModule.forFeature(MoviesEffects, actorsEffects) - ], -}) -export class MovieModule {} - - -### Using the Standalone API - -Feature-level effects are registered in the `providers` array of the route config. The same `provideEffects()` function is used in root-level and feature-level effects. - import { Route } from '@angular/router'; import { provideEffects } from '@ngrx/effects'; @@ -319,17 +291,11 @@ export const routes: Route[] = [ ]; -
- -**Note:** Registering an effects class multiple times, either by `forRoot()`, `forFeature()`, or `provideEffects()`, (for example in different lazy loaded features) will not cause the effects to run multiple times. There is no functional difference between effects loaded by `root` and `feature`; the important difference between the functions is that `root` providers sets up the providers required for effects. - -
- -## Alternative Way of Registering Effects +### Alternative Way of Registering Effects You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`. - + providers: [ MoviesEffects, { @@ -340,37 +306,6 @@ providers: [ ] -
- -The `EffectsModule.forFeature()` method or `provideEffects()` function must be added to the module imports/route config even if you only provide effects over token, and don't pass them through parameters. (Same goes for `EffectsModule.forRoot()`) - -
- -## Standalone API in module-based apps - -If you have a module-based Angular application, you can still use standalone components. NgRx standalone APIs support this workflow as well. - -For module-based apps, you have the `EffectsModule.forRoot([...])` included in the `imports` array of your `AppModule`, which registers the root effects for dependency injection. For a standalone component with feature state/effects registered in its route configuration to successfully run effects, you will need to use the `provideEffects([...])` function in the `providers` array of your `AppModule` to register the injection token. For module-based with standalone components, you will simply have both. - - -import { NgModule } from '@angular/core'; -import { EffectsModule, provideEffects } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -@NgModule({ - imports: [ - EffectsModule.forRoot(MoviesEffects, actorsEffects), - ], - providers: [ - provideEffects(MoviesEffects, actorsEffects) - ] -}) -export class AppModule {} - - - ## Incorporating State If additional metadata is needed to perform an effect besides the initiating action's `type`, we should rely on passed metadata from an action creator's `props` method. @@ -388,7 +323,7 @@ export const login = createAction(
-import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, map } from 'rxjs/operators'; @@ -401,8 +336,11 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { - login$ = createEffect(() => - this.actions$.pipe( + private actions$ = inject(Actions); + private authService = inject(AuthService); + + login$ = createEffect(() => { + return this.actions$.pipe( ofType(LoginPageActions.login), exhaustMap(action => this.authService.login(action.credentials).pipe( @@ -410,24 +348,19 @@ export class AuthEffects { catchError(error => of(AuthApiActions.loginFailure({ error }))) ) ) - ) - ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} + ); + }); } The `login` action has additional `credentials` metadata which is passed to a service to log the specific user into the application. -However, there may be cases when the required metadata is only accessible from state. When state is needed, the RxJS `withLatestFrom` or the @ngrx/effects `concatLatestFrom` operators can be used to provide it. +However, there may be cases when the required metadata is only accessible from state. When state is needed, the RxJS `withLatestFrom` or the @ngrx/effects `concatLatestFrom` operators can be used to provide it. The example below shows the `addBookToCollectionSuccess$` effect displaying a different alert depending on the number of books in the collection state. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { Actions, ofType, createEffect, concatLatestFrom } from '@ngrx/effects'; import { tap } from 'rxjs/operators'; @@ -436,32 +369,30 @@ import * as fromBooks from '../reducers'; @Injectable() export class CollectionEffects { + private actions$ = inject(Actions); + private store = inject(Store<fromBooks.State>); + addBookToCollectionSuccess$ = createEffect( - () => - this.actions$.pipe( + () => { + return this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), - concatLatestFrom(action => this.store.select(fromBooks.getCollectionBookIds)), - tap(([action, bookCollection]) => { + concatLatestFrom(_action => this.store.select(fromBooks.getCollectionBookIds)), + tap(([_action, bookCollection]) => { if (bookCollection.length === 1) { window.alert('Congrats on adding your first book!'); } else { window.alert('You have added book number ' + bookCollection.length); } }) - ), - { dispatch: false } - ); - - constructor( - private actions$: Actions, - private store: Store<fromBooks.State> - ) {} + ); + }, + { dispatch: false }); }
-**Note:** For performance reasons, use a flattening operator like `concatLatestFrom` to prevent the selector from firing until the correct action is dispatched. +For performance reasons, use a flattening operator like `concatLatestFrom` to prevent the selector from firing until the correct action is dispatched.
@@ -474,7 +405,7 @@ Because effects are merely consumers of observables, they can be used without ac For example, imagine we want to track click events and send that data to our monitoring server. This can be done by creating an effect that listens to the `document` `click` event and emits the event data to our server. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, fromEvent } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { createEffect } from '@ngrx/effects'; @@ -483,14 +414,18 @@ import { UserActivityService } from '../services/user-activity.service'; @Injectable() export class UserActivityEffects { - trackUserActivity$ = createEffect(() => - fromEvent(document, 'click').pipe( + private userActivityService = inject(UserActivityService); + + trackUserActivity$ = createEffect(() => { + return fromEvent(document, 'click').pipe( concatMap(event => this.userActivityService.trackUserActivity(event)), - ), { dispatch: false } - ); - - constructor( - private userActivityService: UserActivityService, - ) {} + ); + }, { dispatch: false }); } + +
+ +An example of the `@ngrx/effects` in module-based applications is available at the [following link](https://v17.ngrx.io/guide/effects). + +
diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index 957d205adf..7b7784cf11 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -6,12 +6,12 @@ After all the root effects have been added, the root effect dispatches a `ROOT_E You can see this action as a lifecycle hook, which you can use in order to execute some code after all your root effects have been added. -init$ = createEffect(() => - this.actions$.pipe( +init$ = createEffect(() => { + return this.actions$.pipe( ofType(ROOT_EFFECTS_INIT), map(action => ...) - ) -); + ); +}); ## Effect Metadata @@ -23,18 +23,19 @@ Sometimes you don't want effects to dispatch an action, for example when you onl Usage: -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; import { tap } from 'rxjs/operators'; @Injectable() export class LogEffects { - constructor(private actions$: Actions) {} + private actions$ = inject(Actions); - logActions$ = createEffect(() => - this.actions$.pipe( - tap(action => console.log(action)) - ), { dispatch: false }); + logActions$ = createEffect(() => { + return this.actions$.pipe( + tap(action => console.log(action)) + ); + }, { dispatch: false }); } @@ -57,7 +58,7 @@ To disable resubscriptions add `{useEffectsErrorHandler: false}` to the `createE metadata (second argument). -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, map } from 'rxjs/operators'; @@ -69,9 +70,12 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { + private actions$ = inject(Actions); + private authService = inject(AuthService); + logins$ = createEffect( - () => - this.actions$.pipe( + () => { + return this.actions$.pipe( ofType(LoginPageActions.login), exhaustMap(action => this.authService.login(action.credentials).pipe( @@ -80,14 +84,10 @@ export class AuthEffects { ) ) // Errors are handled and it is safe to disable resubscription - ), + ); + }, { useEffectsErrorHandler: false } ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} } @@ -109,10 +109,10 @@ import { EffectsModule, EFFECTS_ERROR_HANDLER } from '@ngrx/effects'; import { MoviesEffects } from './effects/movies.effects'; import { CustomErrorHandler, isRetryable } from '../custom-error-handler'; -export function effectResubscriptionHandler>T extends Action<( - observable$: Observable>T<, +export function effectResubscriptionHandler<T extends Action>( + observable$: Observable<T>, errorHandler?: CustomErrorHandler -): Observable>T< { +): Observable<T> { return observable$.pipe( retryWhen(errors => errors.pipe( @@ -129,19 +129,21 @@ export function effectResubscriptionHandler>T extends Action<( ); } -@NgModule({ - imports: [EffectsModule.forRoot([MoviesEffects])], - providers: [ - { - provide: EFFECTS_ERROR_HANDLER, - useValue: effectResubscriptionHandler, - }, - { - provide: ErrorHandler, - useClass: CustomErrorHandler - } - ], -}) +bootstrapApplication( + AppComponent, + { + providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: effectResubscriptionHandler, + }, + { + provide: ErrorHandler, + useClass: CustomErrorHandler + } + ], + } +) ## Controlling Effects @@ -181,16 +183,16 @@ import { @Injectable() export class UserEffects implements OnRunEffects { - constructor(private actions$: Actions) {} + private actions$ = inject(Actions); - updateUser$ = createEffect(() => - this.actions$.pipe( + updateUser$ = createEffect(() => { + return this.actions$.pipe( ofType('UPDATE_USER'), tap(action => { console.log(action); }) - ), - { dispatch: false }); + ); + }, { dispatch: false }); ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) { return this.actions$.pipe( diff --git a/projects/ngrx.io/content/guide/effects/operators.md b/projects/ngrx.io/content/guide/effects/operators.md index a7a1dfb6b8..3e1e4d3144 100644 --- a/projects/ngrx.io/content/guide/effects/operators.md +++ b/projects/ngrx.io/content/guide/effects/operators.md @@ -17,7 +17,7 @@ The `ofType` operator takes up to 5 arguments with proper type inference. It can take even more, however the type would be inferred as an `Action` interface. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, map } from 'rxjs/operators'; @@ -30,8 +30,11 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { - login$ = createEffect(() => - this.actions$.pipe( + private actions$ = inject(Actions); + private authService = inject(AuthService); + + login$ = createEffect(() => { + return this.actions$.pipe( // Filters by Action Creator 'login' ofType(LoginPageActions.login), exhaustMap(action => @@ -40,12 +43,7 @@ export class AuthEffects { catchError(error => of(AuthApiActions.loginFailure({ error }))) ) ) - ) - ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} + ); + }); } diff --git a/projects/ngrx.io/src/404-body.html b/projects/ngrx.io/src/404-body.html index a92c9a7912..66826f10f1 100644 --- a/projects/ngrx.io/src/404-body.html +++ b/projects/ngrx.io/src/404-body.html @@ -42,7 +42,7 @@

Resource Not Found

- Powered by The Community ©2015-2020. + Powered by The Community ©2015-2025. Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.

diff --git a/projects/ngrx.io/src/app/layout/footer/footer.component.html b/projects/ngrx.io/src/app/layout/footer/footer.component.html index 433d30b057..4d55545d8c 100644 --- a/projects/ngrx.io/src/app/layout/footer/footer.component.html +++ b/projects/ngrx.io/src/app/layout/footer/footer.component.html @@ -17,7 +17,7 @@

{{node.title}}

- Powered by the Community ©2015-2023. + Powered by the Community ©2015-2025. Code licensed under an