|
| 1 | + |
| 2 | +# ng-update schematic |
| 3 | + |
| 4 | +The `ng-update` schematic consists of multiple migration entry-points where every entry-point targets a specific Angular Material version. |
| 5 | + |
| 6 | +As of right now, we have two migration entry-points that handle the breaking changes for the given target version: |
| 7 | + |
| 8 | +| Target Version | Description | |
| 9 | +|----------------|-------------| |
| 10 | +| V6 | Upgrade from any version to Angular Material 6.0.0 | |
| 11 | +| V7 | Upgrade from any version to Angular Material 7.0.0 | |
| 12 | + |
| 13 | +Note that the migrations run in order if multiple versions are transitively targeted. For example, consider an application which uses Angular Material v5.0.0. In case the developer runs `ng update`, the Angular CLI installs V7 and runs the V6 and V7 migrations in order. |
| 14 | + |
| 15 | +## Update concept |
| 16 | + |
| 17 | +The goal of the update schematic is to automatically migrate code that is affected by breaking changes of the target version. Most of the time, we can apply such automatic migrations, but there are also a few breaking changes that cannot be migrated automatically. |
| 18 | + |
| 19 | +In that case, our goal should be to notify the developer about the breaking change that needs developer attention. |
| 20 | + |
| 21 | +## Transforming TypeScript files |
| 22 | + |
| 23 | +In order to automatically migrate TypeScript source files, we take advantage of the `TSLint` toolkit which involves creating custom rules that allow us to: |
| 24 | + |
| 25 | +* Easily `visit` specific types of TypeScript nodes (e.g. `visitClassDeclaration`) |
| 26 | +* Structure migrations based on the _upgrade data_ or type of migration (different TSLint rules) |
| 27 | +* Easily apply replacements / fixes for specific TypeScript nodes. |
| 28 | +* Easily report breaking changes at TypeScript nodes that cannot be migrated automatically |
| 29 | +* Double check for rule migrations (TSLint always runs rule again after migrations have been applied) |
| 30 | + |
| 31 | +There also other various concepts for transforming TypeScript source files, but most of them don't provide a simple API for replacements and reporting. Read more about the possible approaches below: |
| 32 | + |
| 33 | +|Description | Evaluation | |
| 34 | +|------------|------------| |
| 35 | +| Regular Expressions | Too brittle. No type checking possible. Regular Expression _can_ be used in combination with some real AST walking | |
| 36 | +| TypeScript transforms (no emit) | This would be a good solution that avoids using TSLint. No simple API for reporting and visiting specific types of nodes | |
| 37 | +| Plain TypeScript AST | This would be similar to the TypeScript transforms. Extra effort in creating the replacement API; reporting API; walking logic | |
| 38 | + |
| 39 | +## ## Transforming CSS and HTML files |
| 40 | + |
| 41 | +Since `TSLint` allows us to only visit TypeScript nodes, we can technically just apply migrations for inline styles or templates which are part of the TypeScript AST. |
| 42 | + |
| 43 | +But for our update schematic, we also want to apply migrations for external templates or styles. In order to archive this with TSLint, we have a customized implementation of a `TSLint.RuleWalker`. The custom RuleWalker which is called `ComponentWalker` determines external templates and stylesheets from the _component/directive_ metadata. |
| 44 | + |
| 45 | +The given resource files will then be wrapped inside of an in-memory TypeScript source file that can be applied to the rule walker. This ensures that only referenced resource files will be migrated and also allows us to take advantage of the simple replacement and reporting API from TSLint, |
| 46 | + |
| 47 | +This also makes the rule walker API consistent with the handling of inline resource files. |
| 48 | + |
| 49 | +```ts |
| 50 | +// PSEUDO CODE |
| 51 | +visitExternalTemplate(node: ts.SourceFile) { |
| 52 | + const parsed = parse5.parse(node.getFullText()); this._findOutdatedInputs() |
| 53 | + .forEach(offsetStart => this._addExternalFailure(offsetStart, 'Outdated input', _myFix);} |
| 54 | +``` |
| 55 | +
|
| 56 | +### Upgrade data for target versions |
| 57 | + |
| 58 | +The upgrade data for migrations is separated based on the target version. This is necessary in order to allow migrations run sequentially. For example: |
| 59 | + |
| 60 | +* In V6: `onChange` has been renamed to `changed` |
| 61 | +* In V7: `changed` has been renamed to `onValueChange` |
| 62 | + |
| 63 | +If we would not run the migrations in order, or don't separate the upgrade data, we wouldn't be able to properly handle the migrations for each target version. e.g. someone is on 5.0.0 and *only* wants to upgrade to 6.0.0. In that case he would end up with `onValueChange` because the non-separated upgrade data would just include: _`onChange` >`onValueChange`_ |
| 64 | + |
| 65 | +Also besides separating the upgrade data based on the target version, we split the upgrade data based on the type of code that is affected by these migrations: |
| 66 | + |
| 67 | +* See here: [src/lib/schematics/update/material/data](https://github.com/angular/material2/tree/master/src/lib/schematics/update/material/data) |
| 68 | + |
| 69 | +### Adding upgrade data |
| 70 | + |
| 71 | +Adding upgrade data is now an essential step before breaking changes should be merged into `upstream`. For simple and common breaking changes, there should be already an upgrade data file that just needs the new change inserted. |
| 72 | + |
| 73 | +In case there is no upgrade data for a breaking change, we need to evaluate if there should be a single `misc` migration rule that is tied to that specific breaking change, or if we should create a new migration rule in a more generic way. |
| 74 | +
|
| 75 | +--- |
| 76 | +
|
| 77 | +**Example**: Adding upgrade data for a property rename |
| 78 | +**Scenario**: In Angular Material V7.0.0, we rename `MatRipple#color` to `MatRipple#newColor`. |
| 79 | + |
| 80 | +First, look for an existing upgrade data file that covers similar breaking changes. In that case an existing upgrade data file for `property-names` already exists. Insert the new breaking change within the proper `VersionTarget`. |
| 81 | + |
| 82 | +_src/lib/schematics/update/material/data/property-names.ts_ |
| 83 | +```ts |
| 84 | +export const propertyNames: VersionChanges<MaterialPropertyNameData> = { |
| 85 | + [TargetVersion.V7]: [ |
| 86 | + { |
| 87 | + pr: '{PULL_REQUEST_LINK_FOR_BREAKING_CHANGE}', |
| 88 | + changes: [ |
| 89 | + { |
| 90 | + replace: 'color', |
| 91 | + replaceWith: 'newColor', |
| 92 | + whitelist: { |
| 93 | + classes: ['MatRipple'] |
| 94 | + } |
| 95 | + } |
| 96 | + ] |
| 97 | + } |
| 98 | + ], |
| 99 | + ... |
| 100 | +}; |
| 101 | +``` |
| 102 | +Once the data is inserted into the upgrade data file, the update schematic will properly migrate `MatRipple#color` to `MatRipple#newColor` if someone upgrades to Angular Material V7.0.0. |
| 103 | + |
| 104 | +But that's not all. It's encouraged to add a test-case for the new migration data. In this case, a test case already exists for the type of migration and we just need to add our breaking change to it. Read more about adding a test case in the next section. |
| 105 | + |
| 106 | +### Adding a breaking change to a test case |
| 107 | + |
| 108 | +Considering we added a breaking change to the update schematic, it's encouraged to add a proper test case for the new change that has been added. |
| 109 | + |
| 110 | +In the scenario where a property from `MatRipple` has been renamed in V7, we don't need to create a new test-case file because there is already a test case for the `property-names` upgrade data. In that case, we just need to add the breaking change to the existing test case. |
| 111 | + |
| 112 | +_src/lib/schematics/update/test-cases/v7/property-names_input.ts_ |
| 113 | +```ts |
| 114 | +... |
| 115 | +/** |
| 116 | + * Mock definitions. This test case does not have access to @angular/material. */ |
| 117 | +class MatRipple { |
| 118 | + color: string; |
| 119 | +} |
| 120 | + /* |
| 121 | + * Actual test cases using the previously defined definitions. |
| 122 | + */ |
| 123 | + class A implements OnInit { |
| 124 | + constructor(private a: MatRipple) {} |
| 125 | + ngOnInit() { |
| 126 | + this.a.color = 'primary'; |
| 127 | + } } |
| 128 | +``` |
| 129 | +
|
| 130 | +_src/lib/schematics/update/test-cases/v7/property-names_expected_output.ts_ |
| 131 | +```ts |
| 132 | +... |
| 133 | +/** |
| 134 | + * Mock definitions. This test case does not have access to @angular/material. */ |
| 135 | +class MatRipple { |
| 136 | + color: string; |
| 137 | +} |
| 138 | + /* |
| 139 | + * Actual test cases using the previously defined definitions. |
| 140 | + */ |
| 141 | + class A implements OnInit { |
| 142 | + constructor(private a: MatRipple) {} |
| 143 | + ngOnInit() { |
| 144 | + this.a.newColor = 'primary'; |
| 145 | + } } |
| 146 | +``` |
| 147 | +
|
| 148 | +**Note**: The `_input.ts` file will be just transformed by the V7 migrations and compared to the `_expected_output.ts` file. This means that it's necessary to also include the no longer valid mock declarations to the expected output file. |
0 commit comments