|
| 1 | +import * as ts from 'typescript'; |
| 2 | +import { Rule, chain, Tree } from '@angular-devkit/schematics'; |
| 3 | +import { |
| 4 | + visitTSSourceFiles, |
| 5 | + commitChanges, |
| 6 | + createReplaceChange, |
| 7 | + ReplaceChange, |
| 8 | +} from '../../schematics-core'; |
| 9 | + |
| 10 | +const reactiveComponentModuleText = 'ReactiveComponentModule'; |
| 11 | +const reactiveComponentModuleReplacement = 'LetModule, PushModule'; |
| 12 | +const moduleLocations = { |
| 13 | + imports: ['NgModule', 'Component'], |
| 14 | + exports: ['NgModule'], |
| 15 | +}; |
| 16 | + |
| 17 | +function migrateReactiveComponentModule() { |
| 18 | + return (tree: Tree) => { |
| 19 | + visitTSSourceFiles(tree, (sourceFile) => { |
| 20 | + const componentImports = sourceFile.statements |
| 21 | + .filter(ts.isImportDeclaration) |
| 22 | + .filter(({ moduleSpecifier }) => |
| 23 | + moduleSpecifier.getText(sourceFile).includes('@ngrx/component') |
| 24 | + ); |
| 25 | + |
| 26 | + if (componentImports.length === 0) { |
| 27 | + return; |
| 28 | + } |
| 29 | + |
| 30 | + const ngModuleReplacements = |
| 31 | + findReactiveComponentModuleNgModuleReplacements(sourceFile); |
| 32 | + |
| 33 | + const possibleUsagesOfReactiveComponentModuleCount = |
| 34 | + findPossibleReactiveComponentModuleUsageCount(sourceFile); |
| 35 | + |
| 36 | + const importAdditionReplacements = |
| 37 | + findReactiveComponentModuleImportDeclarationAdditions( |
| 38 | + sourceFile, |
| 39 | + componentImports |
| 40 | + ); |
| 41 | + |
| 42 | + const importUsagesCount = importAdditionReplacements.length; |
| 43 | + |
| 44 | + const jsImportDeclarationReplacements = |
| 45 | + possibleUsagesOfReactiveComponentModuleCount > |
| 46 | + ngModuleReplacements.length + importUsagesCount |
| 47 | + ? importAdditionReplacements |
| 48 | + : findReactiveComponentModuleImportDeclarationReplacements( |
| 49 | + sourceFile, |
| 50 | + componentImports |
| 51 | + ); |
| 52 | + |
| 53 | + const changes = [ |
| 54 | + ...jsImportDeclarationReplacements, |
| 55 | + ...ngModuleReplacements, |
| 56 | + ]; |
| 57 | + |
| 58 | + commitChanges(tree, sourceFile.fileName, changes); |
| 59 | + }); |
| 60 | + }; |
| 61 | +} |
| 62 | + |
| 63 | +function findReactiveComponentModuleImportDeclarationReplacements( |
| 64 | + sourceFile: ts.SourceFile, |
| 65 | + imports: ts.ImportDeclaration[] |
| 66 | +) { |
| 67 | + const changes = imports |
| 68 | + .map((p) => (p?.importClause?.namedBindings as ts.NamedImports)?.elements) |
| 69 | + .reduce( |
| 70 | + (imports, curr) => imports.concat(curr ?? []), |
| 71 | + [] as ts.ImportSpecifier[] |
| 72 | + ) |
| 73 | + .map((specifier) => { |
| 74 | + if (!ts.isImportSpecifier(specifier)) { |
| 75 | + return { hit: false }; |
| 76 | + } |
| 77 | + |
| 78 | + if (specifier.name.text === reactiveComponentModuleText) { |
| 79 | + return { hit: true, specifier, text: specifier.name.text }; |
| 80 | + } |
| 81 | + |
| 82 | + // if import is renamed |
| 83 | + if ( |
| 84 | + specifier.propertyName && |
| 85 | + specifier.propertyName.text === reactiveComponentModuleText |
| 86 | + ) { |
| 87 | + return { hit: true, specifier, text: specifier.propertyName.text }; |
| 88 | + } |
| 89 | + |
| 90 | + return { hit: false }; |
| 91 | + }) |
| 92 | + .filter(({ hit }) => hit) |
| 93 | + .map(({ specifier, text }) => |
| 94 | + !!specifier && !!text |
| 95 | + ? createReplaceChange( |
| 96 | + sourceFile, |
| 97 | + specifier, |
| 98 | + text, |
| 99 | + reactiveComponentModuleReplacement |
| 100 | + ) |
| 101 | + : undefined |
| 102 | + ) |
| 103 | + .filter((change) => !!change) as Array<ReplaceChange>; |
| 104 | + |
| 105 | + return changes; |
| 106 | +} |
| 107 | + |
| 108 | +function findReactiveComponentModuleImportDeclarationAdditions( |
| 109 | + sourceFile: ts.SourceFile, |
| 110 | + imports: ts.ImportDeclaration[] |
| 111 | +) { |
| 112 | + const changes = imports |
| 113 | + .map((p) => (p?.importClause?.namedBindings as ts.NamedImports)?.elements) |
| 114 | + .reduce( |
| 115 | + (imports, curr) => imports.concat(curr ?? []), |
| 116 | + [] as ts.ImportSpecifier[] |
| 117 | + ) |
| 118 | + .map((specifier) => { |
| 119 | + if (!ts.isImportSpecifier(specifier)) { |
| 120 | + return { hit: false }; |
| 121 | + } |
| 122 | + |
| 123 | + if (specifier.name.text === reactiveComponentModuleText) { |
| 124 | + return { hit: true, specifier, text: specifier.name.text }; |
| 125 | + } |
| 126 | + |
| 127 | + // if import is renamed |
| 128 | + if ( |
| 129 | + specifier.propertyName && |
| 130 | + specifier.propertyName.text === reactiveComponentModuleText |
| 131 | + ) { |
| 132 | + return { hit: true, specifier, text: specifier.propertyName.text }; |
| 133 | + } |
| 134 | + |
| 135 | + return { hit: false }; |
| 136 | + }) |
| 137 | + .filter(({ hit }) => hit) |
| 138 | + .map(({ specifier, text }) => |
| 139 | + !!specifier && !!text |
| 140 | + ? createReplaceChange( |
| 141 | + sourceFile, |
| 142 | + specifier, |
| 143 | + text, |
| 144 | + `${text}, ${reactiveComponentModuleReplacement}` |
| 145 | + ) |
| 146 | + : undefined |
| 147 | + ) |
| 148 | + .filter((change) => !!change) as Array<ReplaceChange>; |
| 149 | + |
| 150 | + return changes; |
| 151 | +} |
| 152 | + |
| 153 | +function findPossibleReactiveComponentModuleUsageCount( |
| 154 | + sourceFile: ts.SourceFile |
| 155 | +): number { |
| 156 | + let count = 0; |
| 157 | + ts.forEachChild(sourceFile, (node) => countUsages(node)); |
| 158 | + return count; |
| 159 | + |
| 160 | + function countUsages(node: ts.Node) { |
| 161 | + if (ts.isIdentifier(node) && node.text === reactiveComponentModuleText) { |
| 162 | + count = count + 1; |
| 163 | + } |
| 164 | + |
| 165 | + ts.forEachChild(node, (childNode) => countUsages(childNode)); |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +function findReactiveComponentModuleNgModuleReplacements( |
| 170 | + sourceFile: ts.SourceFile |
| 171 | +) { |
| 172 | + const changes: ReplaceChange[] = []; |
| 173 | + ts.forEachChild(sourceFile, (node) => find(node, changes)); |
| 174 | + return changes; |
| 175 | + |
| 176 | + function find(node: ts.Node, changes: ReplaceChange[]) { |
| 177 | + let change = undefined; |
| 178 | + |
| 179 | + if ( |
| 180 | + ts.isIdentifier(node) && |
| 181 | + node.text === reactiveComponentModuleText && |
| 182 | + ts.isArrayLiteralExpression(node.parent) && |
| 183 | + ts.isPropertyAssignment(node.parent.parent) |
| 184 | + ) { |
| 185 | + const property = node.parent.parent; |
| 186 | + if (ts.isIdentifier(property.name)) { |
| 187 | + const propertyName = String(property.name.escapedText); |
| 188 | + if (Object.keys(moduleLocations).includes(propertyName)) { |
| 189 | + const decorator = property.parent.parent.parent; |
| 190 | + if ( |
| 191 | + ts.isDecorator(decorator) && |
| 192 | + ts.isCallExpression(decorator.expression) && |
| 193 | + ts.isIdentifier(decorator.expression.expression) && |
| 194 | + moduleLocations[propertyName as 'imports' | 'exports'].includes( |
| 195 | + String(decorator.expression.expression.escapedText) |
| 196 | + ) |
| 197 | + ) { |
| 198 | + change = { |
| 199 | + node: node, |
| 200 | + text: node.text, |
| 201 | + }; |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + if (change) { |
| 208 | + changes.push( |
| 209 | + createReplaceChange( |
| 210 | + sourceFile, |
| 211 | + change.node, |
| 212 | + change.text, |
| 213 | + reactiveComponentModuleReplacement |
| 214 | + ) |
| 215 | + ); |
| 216 | + } |
| 217 | + |
| 218 | + ts.forEachChild(node, (childNode) => find(childNode, changes)); |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +export default function (): Rule { |
| 223 | + return chain([migrateReactiveComponentModule()]); |
| 224 | +} |
0 commit comments