-
Notifications
You must be signed in to change notification settings - Fork 162
Update Migrations
Angular CLI provides a way to hook into the ng update
process for the package by exposing a collection of migration schematics.
Besides the schematics setup, the repo migration has a base UpdateChanges
class implementation that works with simple JSON config files and offers built-in functionality for:
- Typescript
- Rename a Class
- HTML Templates (all can be either renamed or removed)
- Selectors
- Directive
- Component
- Bindings
- Bound input property
- Output event binding
- Selectors
Jump to:
The project package.json has the following entry:
"ng-update": {
"migrations": "./migrations/migration-collection.json"
}
This is a special case of a collection definition (usually provided as root "schematics"
property) used by the update+migrate schematic commands. The schema for this JSON is fairly standard with the exception of additional version property:
{
"schematics": {
"migration-01": {
"version": "6.0.0",
"description": "Updates Ignite UI for Angular from v5 to v6",
"factory": "./update-6"
}
//...
The factory is a relative path that should lead to a module index.ts
that exports a function as default that returns a Rule
transformation. This is where manipulations on the source Tree
are performed, including using UpdateChanges
.
Schematic(s) that match the updated package version (and any intermediate versions) will be run as part of ng update igniteui-angular
, so each schematic should only migrate changes made in that specific version.
Because the migrations are valid schematics they can also be executed directly (useful for testing) via ng generate
:
ng g igniteui-angular/migrations/migration-collection.json:migration-XX
Where migration-XX
is the specific migration from the collection json to be run.
Schematics — An Introduction, @angular-devkit/schematics Readme, Update Command Spec
-
Start by adding a new entry to the
migration-collection.json
schematics - set the upcoming version, description and factory path.{ "schematics": { "migration-01": { ... }, "migration-02": { ... }, //... "migration-07": { "version": "6.3.0", "description": "This is the new update for 6.3.0", "factory": "./update-6_3" } } }
-
Create a folder matching the defined factory path, in this case
update-6_3
-
In the new folder create
index.ts
. Here's a boilerplate of the default export looks like this:export default function(): Rule { return (host: Tree, context: SchematicContext) => { // apply changes to host tree }; }
-
[optional] Setup and use
UpdateChanges
. See next section for more.
The class reads a local 'changes' folder to read configurations for various transformations, thus keeping the actual API fairly minimal. To use, instantiate with the current directory and Rule
arguments and then call .applyChanges()
. So the updated migration index.ts
should look like:
export default function(): Rule {
return (host: Tree, context: SchematicContext) => {
// apply changes to host tree
const update = new UpdateChanges(__dirname, host, context);
update.applyChanges();
};
}
To create configurations:
-
[optional] Create the 'changes' folder under the migration factory path if it doesn't exists yet.
-
[optional] Create config JSON files in the 'changes' folder as needed:
File Schema Use for classes.json ../../common/schema/class.schema.json TS Classes selectors.json ../../common/schema/selector.schema.json Component/Directive selectors outputs.json ../../common/schema/binding.schema.json Event Emitters inputs.json ../../common/schema/binding.schema.json Property bindings theme-props.json ../../common/schema/theme-props.schema.json Sass theme function parameters members.json ../../common/schema/members-changes.schema.json Member replacing/renaming in TS files Open and empty object in the file and assign its
$schema
to the respective value from above. This will enable auto-completion (ctrl/cmd + space) and validation for the file. -
Add to the
changes
array. Following the API (ctrl/cmd + space) is probably the best approach, but in general all change objects describe in a similar manner - with aname
orselector
property to find the element in question and either areplaceWith
value orremove
flag to specify the change action.Component/Directive will have a
type
to specify them and input/output bindings have anowner
property with bothselector
andtype
. -
Verify there are no schema errors - make sure your editor has JSON Schema support (VS Code supports by default).
Uses Typescript's AST to find only actual ts.SyntaxKind.Identifier
to avoid replacing property names or string content.
Removing a directive will also match and remove its value if bound in the template, e.g. removing igxRemoveDirective
will remove the entire [igxRemoveDirective]='value'
string from a template.
Just like directives, bindings will also be removed with their respective values. Additionally, at the moment bindings are only matched with the punctuation syntax (not prefixes) like (igxOutput)="value()"
and [igxIntput]="value"
.
In addition to running the automation test, you may want to manually verify that the migration is working properly. To do this, execute the following steps:
- Build the igniteui-angular library, schematics and migration by running:
npm run build:lib
npm run build:schematics
npm run build:migration
This will output the schematic and migration code needed in the dist folder, which is otherwise not outputted if building only with npm run build:lib
.
- Run
npm link
indist/igniteui-angular
folder of the repo.
Then, navigate to the app in which you want to run the migration.
Link to your local repo build:
npm link igniteui-angular
In the consuming project's angular.json
, add projects[app-name].architect.build.options.preserveSymlinks
to true
.
Alternatively, you can copy the content of igniteui-angular/dist/igniteui-angular
and paste it under [consuming-project]/node_modules/igniteui-angular
!
- Run
ng g igniteui-angular/migrations/migration-collection.json:migration-12
Alternative 2
Inside \dist\igniteui-angular
you can call
npm pack
Then, copy/paste the generated .tgz file inside the @angular/cli generated project's root and run
npm install .\igniteui-angular-0.0.1.tgz
ng add igniteui-angular
ng update igniteui-angular
If you need to debug the code that does the migration (the index.ts
file), do the following:
- Add the following object in the
launch.json
file for theigniteui-angular
repo:
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
- Run
node --inspect-brk "C:\Users\hanastasov\AppData\Roaming\npm\node_modules\@angular\cli\bin\ng" g igniteui-angular/migrations/migration-collection.json:migration-12
- Choose "Attach by process ID" inside the VIsual Studio code Debug bar, and choose the correct process in the prompt dropdown.
UpdateChanges
now exposes the getDefaultLanguageService
method which gives access to Angular
's and TypeScript
's language services. Please note, however, that the Angular
's language service extends the TS language service but it only implements a few of its methods. Once you call getDefaultLanguageService
you will see a lot of members that can be used but only some of them will work for template files.
The following methods are used by the Angular
's language service and will work for template and ts
files alike:
Name | Info |
---|---|
getQuickInfoAtPosition | Generates information about an identifier at a given index in a TS or HTML file. Used by IDEs on mouse hover to fill in the tooltip that is shown. |
getCompletionsAtPosition | Returns autocomplete suggestions at a given index in a TS or HTML file. |
getDefinitionAndBoundSpan | Shows the definition locations for a specific identifier at a given index in a TS or HTML file. Used by IDEs for text highlighting. |
getSemanticDiagnostics | Runs a diagnostic check for a TS or HTML file and returns any errors or warnings. Used by IDEs to generate messages for errors/warnings at compile time. |
getReferencesAtPosition | Shows all references of an identifier at a given index in a TS or HTML file. VSCode's Shift + F12. |
Additional docs - link
The ProjectService
is a singleton which manages all projects in its instance. It can be configured with multiple plug-ins to modify or enhance its support for file types. By default, there is a TypeScript
language service governing every file with a .ts
extension. We modify that in the ProjectService
by attaching a global plug-in, which is the Angular
's language service. It extends the TS language service and overrides the methods shown in the above table. It is the plug-in's responsibility to manage how these methods behave and if they should have a fall back logic, which they do. The NG lang service will handle external and internal templates while the TS lang service will be responsible for handling TypeScript
logic in .ts
files.
If you need to get a specific project from the ProjectService
, you can do it like this:
- Create a
ScriptInfo
object which holds information about the file that you're interested in:
const scriptInfo = this.update.projectService.getOrCreateScriptInfoForNormalizedPath(ts.server.asNormalizedPath(entryPath), false);
- Where
entryPath
is the relative path to the file. - The second parameter in this call states that this file is accessed through API and not a client.
- Every
ScriptInfo
knows which projects it's assigned to, by default it's not assigned to any project.
- Open the file:
this.projectService.openClientFile(scriptInfo.fileName);
- By trying to open the file, the
ts server
will resolve all projects in the working directory and will assign that specific file to a project (unless it is an external template). This is the most expensive operation but it will impact performance only on its first call. -
scriptInfo.fileName
is the path to the file, relative or absolute. - The projects are resolved by the
ts server
from the directory of the file up to the firsttsconfig.json
that holds configuration which accepts this type of files. If there are no config files that thisScriptInfo
can be assigned to, thets server
creates anInferredProject
and assigns the file to it.
- Find the project that this
ScriptInfo
is associated with:
const project = this.projectService.findProject(scriptInfo.containingProjects[0].projectName);
-
scriptInfo.containingProjects
is an array of projects that will hold all projects that thisScriptInfo
is associated with. At this point it should be assigned to a single project at most. - Each
ScriptInfo
must know what projects are associated with it and each project must know whatScriptInfo
-s it contains. - To explicitly add a
ScriptInfo
to a project you can call:
project.addMissingFileRoot(scriptInfo.fileName);
This is useful if you're dealing with external templates (.html
files) since they will not be loaded by the ts server
.
All of these steps are needed since we do not know what files will be loaded and we don't know their content. Thus, we cannot assume what imports they might have and we do not know what modules need to be loaded. Normally, a language service can be cheated into working quite easily (link). If it's generated like this, it will have some useful features, such as diagnostics and translation to JS, but it wil not be able to resolve modules and for our needs, we need the whole thing up and running.
In our case the ServerHost
is where the ProjectService
meets Angular
's schematics. This is because the ServerHost
is an interface which must be implemented by a class and it is responsible for navigating a directory tree. For example it could be like the file system which is the case in its implementation by ts.sys
or Angular
's virtual tree, which is the case in our implementation.