From 82ed8db882735ab0a281bf6973d3aa4c7a640b0f Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 09:59:38 -0700 Subject: [PATCH 01/11] WIP: migrate migration tool to schematics --- package.json | 8 +- src/lib/schematics/collection.json | 4 + src/lib/schematics/update/material/color.ts | 15 + .../update/material/component-data.ts | 135 +++++++ .../material/data/attribute-selectors.json | 15 + .../update/material/data/class-names.json | 56 +++ .../update/material/data/css-names.json | 85 +++++ .../material/data/element-selectors.json | 11 + .../update/material/data/export-as-names.json | 1 + .../update/material/data/input-names.json | 203 +++++++++++ .../material/data/method-call-checks.json | 106 ++++++ .../update/material/data/output-names.json | 93 +++++ .../update/material/data/property-names.json | 329 ++++++++++++++++++ .../update/material/extra-stylsheets.ts | 1 + .../update/material/typescript-specifiers.ts | 25 ++ .../update/rules/switchIdentifiersRule.ts | 132 +++++++ .../update/tslint/component-file.ts | 19 + .../update/tslint/component-walker.ts | 131 +++++++ .../update/tslint/find-tslint-binary.ts | 16 + .../update/typescript/identifiers.ts | 12 + .../schematics/update/typescript/imports.ts | 46 +++ .../schematics/update/typescript/literal.ts | 84 +++++ src/lib/schematics/update/update.ts | 17 + 23 files changed, 1540 insertions(+), 4 deletions(-) create mode 100644 src/lib/schematics/update/material/color.ts create mode 100644 src/lib/schematics/update/material/component-data.ts create mode 100644 src/lib/schematics/update/material/data/attribute-selectors.json create mode 100644 src/lib/schematics/update/material/data/class-names.json create mode 100644 src/lib/schematics/update/material/data/css-names.json create mode 100644 src/lib/schematics/update/material/data/element-selectors.json create mode 100644 src/lib/schematics/update/material/data/export-as-names.json create mode 100644 src/lib/schematics/update/material/data/input-names.json create mode 100644 src/lib/schematics/update/material/data/method-call-checks.json create mode 100644 src/lib/schematics/update/material/data/output-names.json create mode 100644 src/lib/schematics/update/material/data/property-names.json create mode 100644 src/lib/schematics/update/material/extra-stylsheets.ts create mode 100644 src/lib/schematics/update/material/typescript-specifiers.ts create mode 100644 src/lib/schematics/update/rules/switchIdentifiersRule.ts create mode 100644 src/lib/schematics/update/tslint/component-file.ts create mode 100644 src/lib/schematics/update/tslint/component-walker.ts create mode 100644 src/lib/schematics/update/tslint/find-tslint-binary.ts create mode 100644 src/lib/schematics/update/typescript/identifiers.ts create mode 100644 src/lib/schematics/update/typescript/imports.ts create mode 100644 src/lib/schematics/update/typescript/literal.ts create mode 100644 src/lib/schematics/update/update.ts diff --git a/package.json b/package.json index c356762fade8..7918bf12e680 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "zone.js": "^0.8.4" }, "devDependencies": { - "@angular-devkit/core": "^0.4.5", - "@angular-devkit/schematics": "^0.4.5", + "@angular-devkit/core": "^0.5.4", + "@angular-devkit/schematics": "^0.5.4", "@angular/bazel": "6.0.0-rc.3", "@angular/compiler-cli": "6.0.0-rc.3", "@angular/http": "6.0.0-rc.3", @@ -51,7 +51,7 @@ "@angular/upgrade": "6.0.0-rc.3", "@bazel/ibazel": "0.3.1", "@google-cloud/storage": "^1.1.1", - "@schematics/angular": "^0.4.5", + "@schematics/angular": "^0.5.4", "@types/chalk": "^0.4.31", "@types/fs-extra": "^4.0.3", "@types/glob": "^5.0.33", @@ -104,8 +104,8 @@ "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", "karma-sauce-launcher": "^1.2.0", - "karma-spec-reporter": "^0.0.32", "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.32", "madge": "^2.2.0", "magic-string": "^0.22.4", "minimatch": "^3.0.4", diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json index bf9b8783077b..b40490ec45e6 100644 --- a/src/lib/schematics/collection.json +++ b/src/lib/schematics/collection.json @@ -9,6 +9,10 @@ "schema": "./shell/schema.json", "aliases": ["material-shell"] }, + "ng-update": { + "description": "Attempts to make fixes to the application to make upgrading from Angular Material 5 to 6 easier", + "factory": "./update/update" + }, // Create a dashboard component "materialDashboard": { "description": "Create a card-based dashboard component", diff --git a/src/lib/schematics/update/material/color.ts b/src/lib/schematics/update/material/color.ts new file mode 100644 index 000000000000..ce8315a677af --- /dev/null +++ b/src/lib/schematics/update/material/color.ts @@ -0,0 +1,15 @@ +import {bold, green, red} from 'chalk'; + +const colorFns = { + 'b': bold, + 'g': green, + 'r': red, +}; + +export function color(message: string): string { + // 'r{{text}}' with red 'text', 'g{{text}}' with green 'text', and 'b{{text}}' with bold 'text'. + return message.replace(/(.)\{\{(.*?)\}\}/g, (m, fnName, text) => { + const fn = colorFns[fnName]; + return fn ? fn(text) : text; + }); +} diff --git a/src/lib/schematics/update/material/component-data.ts b/src/lib/schematics/update/material/component-data.ts new file mode 100644 index 000000000000..5d5ce428b5be --- /dev/null +++ b/src/lib/schematics/update/material/component-data.ts @@ -0,0 +1,135 @@ +export interface MaterialExportAsNameData { + /** The exportAs name to replace. */ + replace: string; + /** The new exportAs name. */ + replaceWith: string; +} + +export interface MaterialElementSelectorData { + /** The element name to replace. */ + replace: string; + /** The new name for the element. */ + replaceWith: string; +} + +export interface MaterialCssNameData { + /** The CSS name to replace. */ + replace: string; + /** The new CSS name. */ + replaceWith: string; + /** Whitelist where this replacement is made. If omitted it is made in all files. */ + whitelist: { + /** Replace this name in CSS files. */ + css?: boolean, + /** Replace this name in HTML files. */ + html?: boolean, + /** Replace this name in TypeScript strings. */ + strings?: boolean + } +} + +export interface MaterialAttributeSelectorData { + /** The attribute name to replace. */ + replace: string; + /** The new name for the attribute. */ + replaceWith: string; +} + +export interface MaterialPropertyNameData { + /** The property name to replace. */ + replace: string; + /** The new name for the property. */ + replaceWith: string; + /** Whitelist where this replacement is made. If omitted it is made for all Classes. */ + whitelist: { + /** Replace the property only when its type is one of the given Classes. */ + classes?: string[]; + } +} + +export interface MaterialClassNameData { + /** The Class name to replace. */ + replace: string; + /** The new name for the Class. */ + replaceWith: string; +} + +export interface MaterialInputNameData { + /** The @Input() name to replace. */ + replace: string; + /** The new name for the @Input(). */ + replaceWith: string; + /** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */ + whitelist?: { + /** Limit to elements with any of these element tags. */ + elements?: string[], + /** Limit to elements with any of these attributes. */ + attributes?: string[], + /** Whether to ignore CSS attribute selectors when doing this replacement. */ + css?: boolean, + } +} + +export interface MaterialOutputNameData { + /** The @Output() name to replace. */ + replace: string; + /** The new name for the @Output(). */ + replaceWith: string; + /** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */ + whitelist?: { + /** Limit to elements with any of these element tags. */ + elements?: string[], + /** Limit to elements with any of these attributes. */ + attributes?: string[], + /** Whether to ignore CSS attribute selectors when doing this replacement. */ + css?: boolean, + } +} + +export interface MaterialMethodCallData { + className: string; + method: string; + invalidArgCounts: { + count: number, + message: string + }[] +} + +type Changes = { + pr: string; + changes: T[] +} + +function getChanges(allChanges: Changes[]): T[] { + return allChanges.reduce((result, changes) => result.concat(changes.changes), []); +} + +/** Export the class name data as part of a module. This means that the data is cached. */ +export const classNames = getChanges(require('./data/class-names.json')); + +/** Export the input names data as part of a module. This means that the data is cached. */ +export const inputNames = getChanges(require('./data/input-names.json')); + +/** Export the output names data as part of a module. This means that the data is cached. */ +export const outputNames = getChanges(require('./data/output-names.json')); + +/** Export the element selectors data as part of a module. This means that the data is cached. */ +export const elementSelectors = + getChanges(require('./data/element-selectors.json')); + +/** Export the attribute selectors data as part of a module. This means that the data is cached. */ +export const exportAsNames = + getChanges(require('./data/export-as-names.json')); + +/** Export the attribute selectors data as part of a module. This means that the data is cached. */ +export const attributeSelectors = + getChanges(require('./data/attribute-selectors.json')); + +/** Export the property names as part of a module. This means that the data is cached. */ +export const propertyNames = + getChanges(require('./data/property-names.json')); + +export const methodCallChecks = + getChanges(require('./data/method-call-checks.json')); + +export const cssNames = getChanges(require('./data/css-names.json')); diff --git a/src/lib/schematics/update/material/data/attribute-selectors.json b/src/lib/schematics/update/material/data/attribute-selectors.json new file mode 100644 index 000000000000..808fe83a03c1 --- /dev/null +++ b/src/lib/schematics/update/material/data/attribute-selectors.json @@ -0,0 +1,15 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10257", + "changes": [ + { + "replace": "cdkPortalHost", + "replaceWith": "cdkPortalOutlet" + }, + { + "replace": "portalHost", + "replaceWith": "cdkPortalOutlet" + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/class-names.json b/src/lib/schematics/update/material/data/class-names.json new file mode 100644 index 000000000000..fe479bb618db --- /dev/null +++ b/src/lib/schematics/update/material/data/class-names.json @@ -0,0 +1,56 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10161", + "changes": [ + { + "replace": "ConnectedOverlayDirective", + "replaceWith": "CdkConnectedOverlay" + }, + { + "replace": "OverlayOrigin", + "replaceWith": "CdkOverlayOrigin" + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10267", + "changes": [ + { + "replace": "ObserveContent", + "replaceWith": "CdkObserveContent" + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10291", + "changes": [ + { + "replace": "FloatPlaceholderType", + "replaceWith": "FloatLabelType" + }, + { + "replace": "MAT_PLACEHOLDER_GLOBAL_OPTIONS", + "replaceWith": "MAT_LABEL_GLOBAL_OPTIONS" + }, + { + "replace": "PlaceholderOptions", + "replaceWith": "LabelOptions" + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10325", + "changes": [ + { + "replace": "FocusTrapDirective", + "replaceWith": "CdkTrapFocus" + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/css-names.json b/src/lib/schematics/update/material/data/css-names.json new file mode 100644 index 000000000000..3fb7f69702f2 --- /dev/null +++ b/src/lib/schematics/update/material/data/css-names.json @@ -0,0 +1,85 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10296", + "changes": [ + { + "replace": "mat-form-field-placeholder", + "replaceWith": "mat-form-field-label" + }, + { + "replace": "mat-form-field-placeholder-wrapper", + "replaceWith": "mat-form-field-label-wrapper" + }, + { + "replace": "mat-input-container", + "replaceWith": "mat-form-field" + }, + { + "replace": "mat-input-flex", + "replaceWith": "mat-form-field-flex" + }, + { + "replace": "mat-input-hint-spacer", + "replaceWith": "mat-form-field-hint-spacer" + }, + { + "replace": "mat-input-hint-wrapper", + "replaceWith": "mat-form-field-hint-wrapper" + }, + { + "replace": "mat-input-infix", + "replaceWith": "mat-form-field-infix" + }, + { + "replace": "mat-input-invalid", + "replaceWith": "mat-form-field-invalid" + }, + { + "replace": "mat-input-placeholder", + "replaceWith": "mat-form-field-label" + }, + { + "replace": "mat-input-placeholder-wrapper", + "replaceWith": "mat-form-field-label-wrapper" + }, + { + "replace": "mat-input-prefix", + "replaceWith": "mat-form-field-prefix" + }, + { + "replace": "mat-input-ripple", + "replaceWith": "mat-form-field-ripple" + }, + { + "replace": "mat-input-subscript-wrapper", + "replaceWith": "mat-form-field-subscript-wrapper" + }, + { + "replace": "mat-input-suffix", + "replaceWith": "mat-form-field-suffix" + }, + { + "replace": "mat-input-underline", + "replaceWith": "mat-form-field-underline" + }, + { + "replace": "mat-input-wrapper", + "replaceWith": "mat-form-field-wrapper" + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10325", + "changes": [ + { + "replace": "$mat-font-family", + "replaceWith": "Roboto, 'Helvetica Neue', sans-serif", + "whitelist": { + "css": true + } + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/element-selectors.json b/src/lib/schematics/update/material/data/element-selectors.json new file mode 100644 index 000000000000..6e801a29c870 --- /dev/null +++ b/src/lib/schematics/update/material/data/element-selectors.json @@ -0,0 +1,11 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10297", + "changes": [ + { + "replace": "mat-input-container", + "replaceWith": "mat-form-field" + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/export-as-names.json b/src/lib/schematics/update/material/data/export-as-names.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/src/lib/schematics/update/material/data/export-as-names.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/lib/schematics/update/material/data/input-names.json b/src/lib/schematics/update/material/data/input-names.json new file mode 100644 index 000000000000..535ca34ab072 --- /dev/null +++ b/src/lib/schematics/update/material/data/input-names.json @@ -0,0 +1,203 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10161", + "changes": [ + { + "replace": "origin", + "replaceWith": "cdkConnectedOverlayOrigin", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "positions", + "replaceWith": "cdkConnectedOverlayPositions", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "offsetX", + "replaceWith": "cdkConnectedOverlayOffsetX", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "offsetY", + "replaceWith": "cdkConnectedOverlayOffsetY", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "width", + "replaceWith": "cdkConnectedOverlayWidth", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "height", + "replaceWith": "cdkConnectedOverlayHeight", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "minWidth", + "replaceWith": "cdkConnectedOverlayMinWidth", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "minHeight", + "replaceWith": "cdkConnectedOverlayMinHeight", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "backdropClass", + "replaceWith": "cdkConnectedOverlayBackdropClass", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "scrollStrategy", + "replaceWith": "cdkConnectedOverlayScrollStrategy", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "open", + "replaceWith": "cdkConnectedOverlayOpen", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + }, + { + "replace": "hasBackdrop", + "replaceWith": "cdkConnectedOverlayHasBackdrop", + "whitelist": { + "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10218", + "changes": [ + { + "replace": "align", + "replaceWith": "labelPosition", + "whitelist": { + "elements": ["mat-radio-group", "mat-radio-button"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10279", + "changes": [ + { + "replace": "align", + "replaceWith": "position", + "whitelist": { + "elements": ["mat-drawer", "mat-sidenav"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10294", + "changes": [ + { + "replace": "dividerColor", + "replaceWith": "color", + "whitelist": { + "elements": ["mat-form-field"] + } + }, + { + "replace": "floatPlaceholder", + "replaceWith": "floatLabel", + "whitelist": { + "elements": ["mat-form-field"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10309", + "changes": [ + { + "replace": "mat-dynamic-height", + "replaceWith": "dynamicHeight", + "whitelist": { + "elements": ["mat-tab-group"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10342", + "changes": [ + { + "replace": "align", + "replaceWith": "labelPosition", + "whitelist": { + "elements": ["mat-checkbox"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10344", + "changes": [ + { + "replace": "tooltip-position", + "replaceWith": "matTooltipPosition", + "whitelist": { + "attributes": ["matTooltip"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10373", + "changes": [ + { + "replace": "thumb-label", + "replaceWith": "thumbLabel", + "whitelist": { + "elements": ["mat-slider"] + } + }, + { + "replace": "tick-interval", + "replaceWith": "tickInterval", + "whitelist": { + "elements": ["mat-slider"] + } + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/method-call-checks.json b/src/lib/schematics/update/material/data/method-call-checks.json new file mode 100644 index 000000000000..b03965580899 --- /dev/null +++ b/src/lib/schematics/update/material/data/method-call-checks.json @@ -0,0 +1,106 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/9190", + "changes": [ + { + "className": "NativeDateAdapter", + "method": "constructor", + "invalidArgCounts": [ + { + "count": 1, + "message": "\"g{{platform}}\" is now required as a second argument" + } + ] + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10319", + "changes": [ + { + "className": "MatAutocomplete", + "method": "constructor", + "invalidArgCounts": [ + { + "count": 2, + "message": "\"g{{default}}\" is now required as a third argument" + } + ] + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10325", + "changes": [ + { + "className": "FocusMonitor", + "method": "monitor", + "invalidArgCounts": [ + { + "count": 3, + "message": "The \"r{{renderer}}\" argument has been removed" + } + ] + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10344", + "changes": [ + { + "className": "MatTooltip", + "method": "constructor", + "invalidArgCounts": [ + { + "count": 11, + "message": "\"g{{_defaultOptions}}\" is now required as a twelfth argument" + } + ] + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10389", + "changes": [ + { + "className": "MatIconRegistry", + "method": "constructor", + "invalidArgCounts": [ + { + "count": 2, + "message": "\"g{{document}}\" is now required as a third argument" + } + ] + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/9775", + "changes": [ + { + "className": "MatCalendar", + "method": "constructor", + "invalidArgCounts": [ + { + "count": 6, + "message": "\"r{{_elementRef}}\" and \"r{{_ngZone}}\" arguments have been removed" + }, + { + "count": 7, + "message": "\"r{{_elementRef}}\", \"r{{_ngZone}}\", and \"r{{_dir}}\" arguments have been removed" + } + ] + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/output-names.json b/src/lib/schematics/update/material/data/output-names.json new file mode 100644 index 000000000000..5b68b8c54f81 --- /dev/null +++ b/src/lib/schematics/update/material/data/output-names.json @@ -0,0 +1,93 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10163", + "changes": [ + { + "replace": "change", + "replaceWith": "selectionChange", + "whitelist": { + "elements": ["mat-select"] + } + }, + { + "replace": "onClose", + "replaceWith": "closed", + "whitelist": { + "elements": ["mat-select"] + } + }, + { + "replace": "onOpen", + "replaceWith": "opened", + "whitelist": { + "elements": ["mat-select"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10279", + "changes": [ + { + "replace": "align-changed", + "replaceWith": "positionChanged", + "whitelist": { + "elements": ["mat-drawer", "mat-sidenav"] + } + }, + { + "replace": "close", + "replaceWith": "closed", + "whitelist": { + "elements": ["mat-drawer", "mat-sidenav"] + } + }, + { + "replace": "open", + "replaceWith": "opened", + "whitelist": { + "elements": ["mat-drawer", "mat-sidenav"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10309", + "changes": [ + { + "replace": "selectChange", + "replaceWith": "selectedTabChange", + "whitelist": { + "elements": ["mat-tab-group"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10311", + "changes": [ + { + "replace": "remove", + "replaceWith": "removed", + "whitelist": { + "attributes": ["mat-chip", "mat-basic-chip"], + "elements": ["mat-chip", "mat-basic-chip"] + } + }, + { + "replace": "destroy", + "replaceWith": "destroyed", + "whitelist": { + "attributes": ["mat-chip", "mat-basic-chip"], + "elements": ["mat-chip", "mat-basic-chip"] + } + } + ] + } +] diff --git a/src/lib/schematics/update/material/data/property-names.json b/src/lib/schematics/update/material/data/property-names.json new file mode 100644 index 000000000000..1e138a64bcf8 --- /dev/null +++ b/src/lib/schematics/update/material/data/property-names.json @@ -0,0 +1,329 @@ +[ + { + "pr": "https://github.com/angular/material2/pull/10161", + "changes": [ + { + "replace": "_deprecatedOrigin", + "replaceWith": "origin", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedPositions", + "replaceWith": "positions", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedOffsetX", + "replaceWith": "offsetX", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedOffsetY", + "replaceWith": "offsetY", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedWidth", + "replaceWith": "width", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedHeight", + "replaceWith": "height", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedMinWidth", + "replaceWith": "minWidth", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedMinHeight", + "replaceWith": "minHeight", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedBackdropClass", + "replaceWith": "backdropClass", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedScrollStrategy", + "replaceWith": "scrollStrategy", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedOpen", + "replaceWith": "open", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + }, + { + "replace": "_deprecatedHasBackdrop", + "replaceWith": "hasBackdrop", + "whitelist": { + "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10163", + "changes": [ + { + "replace": "change", + "replaceWith": "selectionChange", + "whitelist": { + "classes": ["MatSelect"] + } + }, + { + "replace": "onOpen", + "replaceWith": "openedChange.pipe(filter(isOpen => isOpen))", + "whitelist": { + "classes": ["MatSelect"] + } + }, + { + "replace": "onClose", + "replaceWith": "openedChange.pipe(filter(isOpen => !isOpen))", + "whitelist": { + "classes": ["MatSelect"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10218", + "changes": [ + { + "replace": "align", + "replaceWith": "labelPosition", + "whitelist": { + "classes": ["MatRadioGroup", "MatRadioButton"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10253", + "changes": [ + { + "replace": "extraClasses", + "replaceWith": "panelClass", + "whitelist": { + "classes": ["MatSnackBarConfig"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10257", + "changes": [ + { + "replace": "_deprecatedPortal", + "replaceWith": "portal", + "whitelist": { + "classes": ["CdkPortalOutlet"] + } + }, + { + "replace": "_deprecatedPortalHost", + "replaceWith": "portal", + "whitelist": { + "classes": ["CdkPortalOutlet"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10279", + "changes": [ + { + "replace": "align", + "replaceWith": "position", + "whitelist": { + "classes": ["MatDrawer", "MatSidenav"] + } + }, + { + "replace": "onAlignChanged", + "replaceWith": "onPositionChanged", + "whitelist": { + "classes": ["MatDrawer", "MatSidenav"] + } + }, + { + "replace": "onOpen", + "replaceWith": "openedChange.pipe(filter(isOpen => isOpen))", + "whitelist": { + "classes": ["MatDrawer", "MatSidenav"] + } + }, + { + "replace": "onClose", + "replaceWith": "openedChange.pipe(filter(isOpen => !isOpen))", + "whitelist": { + "classes": ["MatDrawer", "MatSidenav"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10293", + "changes": [ + { + "replace": "shouldPlaceholderFloat", + "replaceWith": "shouldLabelFloat", + "whitelist": { + "classes": ["MatFormFieldControl", "MatSelect"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10294", + "changes": [ + { + "replace": "dividerColor", + "replaceWith": "color", + "whitelist": { + "classes": ["MatFormField"] + } + }, + { + "replace": "floatPlaceholder", + "replaceWith": "floatLabel", + "whitelist": { + "classes": ["MatFormField"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10309", + "changes": [ + { + "replace": "selectChange", + "replaceWith": "selectedTabChange", + "whitelist": { + "classes": ["MatTabGroup"] + } + }, + { + "replace": "_dynamicHeightDeprecated", + "replaceWith": "dynamicHeight", + "whitelist": { + "classes": ["MatTabGroup"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10311", + "changes": [ + { + "replace": "destroy", + "replaceWith": "destroyed", + "whitelist": { + "classes": ["MatChip"] + } + }, + { + "replace": "onRemove", + "replaceWith": "removed", + "whitelist": { + "classes": ["MatChip"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10342", + "changes": [ + { + "replace": "align", + "replaceWith": "labelPosition", + "whitelist": { + "classes": ["MatCheckbox"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10344", + "changes": [ + { + "replace": "_positionDeprecated", + "replaceWith": "position", + "whitelist": { + "classes": ["MatTooltip"] + } + } + ] + }, + + + { + "pr": "https://github.com/angular/material2/pull/10373", + "changes": [ + { + "replace": "_thumbLabelDeprecated", + "replaceWith": "thumbLabel", + "whitelist": { + "classes": ["MatSlider"] + } + }, + { + "replace": "_tickIntervalDeprecated", + "replaceWith": "tickInterval", + "whitelist": { + "classes": ["MatSlider"] + } + } + ] + } +] diff --git a/src/lib/schematics/update/material/extra-stylsheets.ts b/src/lib/schematics/update/material/extra-stylsheets.ts new file mode 100644 index 000000000000..2c670a180da8 --- /dev/null +++ b/src/lib/schematics/update/material/extra-stylsheets.ts @@ -0,0 +1 @@ +export const EXTRA_STYLESHEETS_GLOB_KEY = 'MD_EXTRA_STYLESHEETS_GLOB'; diff --git a/src/lib/schematics/update/material/typescript-specifiers.ts b/src/lib/schematics/update/material/typescript-specifiers.ts new file mode 100644 index 000000000000..b3c3c000efb4 --- /dev/null +++ b/src/lib/schematics/update/material/typescript-specifiers.ts @@ -0,0 +1,25 @@ +import * as ts from 'typescript'; +import {getExportDeclaration, getImportDeclaration} from '../typescript/imports'; + +/** Name of the Angular Material module specifier. */ +export const materialModuleSpecifier = '@angular/material'; + +/** Name of the Angular CDK module specifier. */ +export const cdkModuleSpecifier = '@angular/cdk'; + +/** Whether the specified node is part of an Angular Material import declaration. */ +export function isMaterialImportDeclaration(node: ts.Node) { + return isMaterialDeclaration(getImportDeclaration(node)); +} + +/** Whether the specified node is part of an Angular Material export declaration. */ +export function isMaterialExportDeclaration(node: ts.Node) { + return getExportDeclaration(getImportDeclaration(node)); +} + +/** Whether the declaration is part of Angular Material. */ +function isMaterialDeclaration(declaration: ts.ImportDeclaration | ts.ExportDeclaration) { + const moduleSpecifier = declaration.moduleSpecifier.getText(); + return moduleSpecifier.indexOf(materialModuleSpecifier) !== -1|| + moduleSpecifier.indexOf(cdkModuleSpecifier) !== -1; +} \ No newline at end of file diff --git a/src/lib/schematics/update/rules/switchIdentifiersRule.ts b/src/lib/schematics/update/rules/switchIdentifiersRule.ts new file mode 100644 index 000000000000..af59a38b976f --- /dev/null +++ b/src/lib/schematics/update/rules/switchIdentifiersRule.ts @@ -0,0 +1,132 @@ +import {green, red} from 'chalk'; +import {relative} from 'path'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {classNames} from '../material/component-data'; +import { + isMaterialExportDeclaration, + isMaterialImportDeclaration, +} from '../material/typescript-specifiers'; +import {getOriginalSymbolFromNode} from '../typescript/identifiers'; +import { + isExportSpecifierNode, + isImportSpecifierNode, + isNamespaceImportNode +} from '../typescript/imports'; + +/** + * Rule that walks through every identifier that is part of Angular Material and replaces the + * outdated name with the new one. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new SwitchIdentifiersWalker(sourceFile, this.getOptions(), program)); + } +} + +export class SwitchIdentifiersWalker extends ProgramAwareRuleWalker { + + /** List of Angular Material declarations inside of the current source file. */ + materialDeclarations: ts.Declaration[] = []; + + /** List of Angular Material namespace declarations in the current source file. */ + materialNamespaceDeclarations: ts.Declaration[] = []; + + /** Method that is called for every identifier inside of the specified project. */ + visitIdentifier(identifier: ts.Identifier) { + // Store Angular Material namespace identifers in a list of declarations. + // Namespace identifiers can be: `import * as md from '@angular/material';` + this._storeNamespaceImports(identifier); + + // For identifiers that aren't listed in the className data, the whole check can be + // skipped safely. + if (!classNames.some(data => data.replace === identifier.text)) { + return; + } + + const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); + + // If the symbol is not defined or could not be resolved, just skip the following identifier + // checks. + if (!symbol || !symbol.name || symbol.name === 'unknown') { + console.error(`Could not resolve symbol for identifier "${identifier.text}" ` + + `in file ${this._getRelativeFileName()}`); + return; + } + + // For export declarations that are referring to Angular Material, the identifier should be + // switched to the new name. + if (isExportSpecifierNode(identifier) && isMaterialExportDeclaration(identifier)) { + return this.createIdentifierFailure(identifier, symbol); + } + + // For import declarations that are referring to Angular Material, the value declarations + // should be stored so that other identifiers in the file can be compared. + if (isImportSpecifierNode(identifier) && isMaterialImportDeclaration(identifier)) { + this.materialDeclarations.push(symbol.valueDeclaration); + } + + // For identifiers that are not part of an import or export, the list of Material declarations + // should be checked to ensure that only identifiers of Angular Material are updated. + // Identifiers that are imported through an Angular Material namespace will be updated. + else if (this.materialDeclarations.indexOf(symbol.valueDeclaration) === -1 && + !this._isIdentifierFromNamespace(identifier)) { + return; + } + + return this.createIdentifierFailure(identifier, symbol); + } + + /** Creates a failure and replacement for the specified identifier. */ + private createIdentifierFailure(identifier: ts.Identifier, symbol: ts.Symbol) { + let classData = classNames.find( + data => data.replace === symbol.name || data.replace === identifier.text); + + if (!classData) { + console.error(`Could not find updated name for identifier "${identifier.getText()}" in ` + + ` in file ${this._getRelativeFileName()}.`); + return; + } + + const replacement = this.createReplacement( + identifier.getStart(), identifier.getWidth(), classData.replaceWith); + + this.addFailureAtNode( + identifier, + `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + + ` "${green(classData.replaceWith)}"`, + replacement); + } + + /** Checks namespace imports from Angular Material and stores them in a list. */ + private _storeNamespaceImports(identifier: ts.Identifier) { + // In some situations, developers will import Angular Material completely using a namespace + // import. This is not recommended, but should be still handled in the migration tool. + if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { + const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); + + if (symbol) { + return this.materialNamespaceDeclarations.push(symbol.valueDeclaration); + } + } + } + + /** Checks whether the given identifier is part of the Material namespace. */ + private _isIdentifierFromNamespace(identifier: ts.Identifier) { + if (identifier.parent && identifier.parent.kind !== ts.SyntaxKind.PropertyAccessExpression) { + return; + } + + const propertyExpression = identifier.parent as ts.PropertyAccessExpression; + const expressionSymbol = getOriginalSymbolFromNode(propertyExpression.expression, + this.getTypeChecker()); + + return this.materialNamespaceDeclarations.indexOf(expressionSymbol.valueDeclaration) !== -1; + } + + /** Returns the current source file path relative to the root directory of the project. */ + private _getRelativeFileName(): string { + return relative(this.getProgram().getCurrentDirectory(), this.getSourceFile().fileName); + } +} diff --git a/src/lib/schematics/update/tslint/component-file.ts b/src/lib/schematics/update/tslint/component-file.ts new file mode 100644 index 000000000000..8a6445c5e5cc --- /dev/null +++ b/src/lib/schematics/update/tslint/component-file.ts @@ -0,0 +1,19 @@ +import * as ts from 'typescript'; + +export type ExternalResource = ts.SourceFile; + +/** + * Creates a fake TypeScript source file that can contain content of templates or stylesheets. + * The fake TypeScript source file then can be passed to TSLint in combination with a rule failure. + */ +export function createComponentFile(filePath: string, content: string): ExternalResource { + const sourceFile = ts.createSourceFile(filePath, `\`${content}\``, ts.ScriptTarget.ES5); + const _getFullText = sourceFile.getFullText; + + sourceFile.getFullText = function() { + const text = _getFullText.apply(sourceFile); + return text.substring(1, text.length - 1); + }.bind(sourceFile); + + return sourceFile; +} diff --git a/src/lib/schematics/update/tslint/component-walker.ts b/src/lib/schematics/update/tslint/component-walker.ts new file mode 100644 index 000000000000..9101ef2abb20 --- /dev/null +++ b/src/lib/schematics/update/tslint/component-walker.ts @@ -0,0 +1,131 @@ +/** + * TSLint custom walker implementation that also visits external and inline templates. + */ +import {existsSync, readFileSync} from 'fs' +import {dirname, join, resolve} from 'path'; +import {Fix, IOptions, RuleFailure, RuleWalker} from 'tslint'; +import * as ts from 'typescript'; +import {getLiteralTextWithoutQuotes} from '../typescript/literal'; +import {createComponentFile, ExternalResource} from "./component-file"; + +/** + * Custom TSLint rule walker that identifies Angular components and visits specific parts of + * the component metadata. + */ +export class ComponentWalker extends RuleWalker { + + protected visitInlineTemplate(template: ts.StringLiteral) {} + protected visitInlineStylesheet(stylesheet: ts.StringLiteral) {} + + protected visitExternalTemplate(template: ExternalResource) {} + protected visitExternalStylesheet(stylesheet: ExternalResource) {} + + private skipFiles: Set; + + constructor(sourceFile: ts.SourceFile, options: IOptions, skipFiles: string[] = []) { + super(sourceFile, options); + this.skipFiles = new Set(skipFiles.map(p => resolve(p))); + } + + visitNode(node: ts.Node) { + if (node.kind === ts.SyntaxKind.CallExpression) { + const callExpression = node as ts.CallExpression; + const callExpressionName = callExpression.expression.getText(); + + if (callExpressionName === 'Component' || callExpressionName === 'Directive') { + this._visitDirectiveCallExpression(callExpression); + } + } + + super.visitNode(node); + } + + private _visitDirectiveCallExpression(callExpression: ts.CallExpression) { + const directiveMetadata = callExpression.arguments[0] as ts.ObjectLiteralExpression; + + if (!directiveMetadata) { + return; + } + + for (const property of directiveMetadata.properties as ts.NodeArray) { + const propertyName = property.name.getText(); + const initializerKind = property.initializer.kind; + + if (propertyName === 'template') { + this.visitInlineTemplate(property.initializer as ts.StringLiteral) + } + + if (propertyName === 'templateUrl' && initializerKind === ts.SyntaxKind.StringLiteral) { + this._reportExternalTemplate(property.initializer as ts.StringLiteral); + } + + if (propertyName === 'styles' && initializerKind === ts.SyntaxKind.ArrayLiteralExpression) { + this._reportInlineStyles(property.initializer as ts.ArrayLiteralExpression); + } + + if (propertyName === 'styleUrls' && initializerKind === ts.SyntaxKind.ArrayLiteralExpression) { + this._visitExternalStylesArrayLiteral(property.initializer as ts.ArrayLiteralExpression); + } + } + } + + private _reportInlineStyles(inlineStyles: ts.ArrayLiteralExpression) { + inlineStyles.elements.forEach(element => { + this.visitInlineStylesheet(element as ts.StringLiteral); + }); + } + + private _visitExternalStylesArrayLiteral(styleUrls: ts.ArrayLiteralExpression) { + styleUrls.elements.forEach(styleUrlLiteral => { + const styleUrl = getLiteralTextWithoutQuotes(styleUrlLiteral as ts.StringLiteral); + const stylePath = resolve(join(dirname(this.getSourceFile().fileName), styleUrl)); + + if (!this.skipFiles.has(stylePath)) { + this._reportExternalStyle(stylePath); + } + }) + } + + private _reportExternalTemplate(templateUrlLiteral: ts.StringLiteral) { + const templateUrl = getLiteralTextWithoutQuotes(templateUrlLiteral); + const templatePath = resolve(join(dirname(this.getSourceFile().fileName), templateUrl)); + + if (this.skipFiles.has(templatePath)) { + return; + } + + // Check if the external template file exists before proceeding. + if (!existsSync(templatePath)) { + console.error(`PARSE ERROR: ${this.getSourceFile().fileName}:` + + ` Could not find template: "${templatePath}".`); + process.exit(1); + } + + // Create a fake TypeScript source file that includes the template content. + const templateFile = createComponentFile(templatePath, readFileSync(templatePath, 'utf8')); + + this.visitExternalTemplate(templateFile); + } + + public _reportExternalStyle(stylePath: string) { + // Check if the external stylesheet file exists before proceeding. + if (!existsSync(stylePath)) { + console.error(`PARSE ERROR: ${this.getSourceFile().fileName}:` + + ` Could not find stylesheet: "${stylePath}".`); + process.exit(1); + } + + // Create a fake TypeScript source file that includes the stylesheet content. + const stylesheetFile = createComponentFile(stylePath, readFileSync(stylePath, 'utf8')); + + this.visitExternalStylesheet(stylesheetFile); + } + + /** Creates a TSLint rule failure for the given external resource. */ + protected addExternalResourceFailure(file: ExternalResource, message: string, fix?: Fix) { + const ruleFailure = new RuleFailure(file, file.getStart(), file.getEnd(), + message, this.getRuleName(), fix); + + this.addFailure(ruleFailure); + } +} diff --git a/src/lib/schematics/update/tslint/find-tslint-binary.ts b/src/lib/schematics/update/tslint/find-tslint-binary.ts new file mode 100644 index 000000000000..2326a2101053 --- /dev/null +++ b/src/lib/schematics/update/tslint/find-tslint-binary.ts @@ -0,0 +1,16 @@ +import {resolve} from 'path'; +import {existsSync} from 'fs'; + +// This import lacks of type definitions. +const resolveBinSync = require('resolve-bin').sync; + +/** Finds the path to the TSLint CLI binary. */ +export function findTslintBinaryPath() { + const defaultPath = resolve(__dirname, '..', 'node_modules', 'tslint', 'bin', 'tslint'); + + if (existsSync(defaultPath)) { + return defaultPath; + } else { + return resolveBinSync('tslint', 'tslint'); + } +} \ No newline at end of file diff --git a/src/lib/schematics/update/typescript/identifiers.ts b/src/lib/schematics/update/typescript/identifiers.ts new file mode 100644 index 000000000000..7371c42c45b7 --- /dev/null +++ b/src/lib/schematics/update/typescript/identifiers.ts @@ -0,0 +1,12 @@ +import * as ts from 'typescript'; + +/** Returns the original symbol from an node. */ +export function getOriginalSymbolFromNode(node: ts.Node, checker: ts.TypeChecker) { + const baseSymbol = checker.getSymbolAtLocation(node); + + if (baseSymbol && baseSymbol.flags & ts.SymbolFlags.Alias) { + return checker.getAliasedSymbol(baseSymbol); + } + + return baseSymbol; +} diff --git a/src/lib/schematics/update/typescript/imports.ts b/src/lib/schematics/update/typescript/imports.ts new file mode 100644 index 000000000000..e5d01f52873a --- /dev/null +++ b/src/lib/schematics/update/typescript/imports.ts @@ -0,0 +1,46 @@ +import * as ts from 'typescript'; + +/** Checks whether the given node is part of an import specifier node. */ +export function isImportSpecifierNode(node: ts.Node) { + return isPartOfKind(node, ts.SyntaxKind.ImportSpecifier); +} + +/** Checks whether the given node is part of an export specifier node. */ +export function isExportSpecifierNode(node: ts.Node) { + return isPartOfKind(node, ts.SyntaxKind.ExportSpecifier); +} + +/** Checks whether the given node is part of a namespace import. */ +export function isNamespaceImportNode(node: ts.Node) { + return isPartOfKind(node, ts.SyntaxKind.NamespaceImport); +} + +/** Finds the parent import declaration of a given TypeScript node. */ +export function getImportDeclaration(node: ts.Node) { + return findDeclaration(node, ts.SyntaxKind.ImportDeclaration) as ts.ImportDeclaration; +} + +/** Finds the parent export declaration of a given TypeScript node */ +export function getExportDeclaration(node: ts.Node) { + return findDeclaration(node, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration; +} + +/** Finds the specified declaration for the given node by walking up the TypeScript nodes. */ +function findDeclaration(node: ts.Node, kind: T) { + while (node.kind !== kind) { + node = node.parent; + } + + return node; +} + +/** Checks whether the given node is part of another TypeScript Node with the specified kind. */ +function isPartOfKind(node: ts.Node, kind: T): boolean { + if (node.kind === kind) { + return true; + } else if (node.kind === ts.SyntaxKind.SourceFile) { + return false; + } + + return isPartOfKind(node.parent, kind); +} diff --git a/src/lib/schematics/update/typescript/literal.ts b/src/lib/schematics/update/typescript/literal.ts new file mode 100644 index 000000000000..ce45ee013c91 --- /dev/null +++ b/src/lib/schematics/update/typescript/literal.ts @@ -0,0 +1,84 @@ +import * as ts from 'typescript'; + +/** Returns the text of a string literal without the quotes. */ +export function getLiteralTextWithoutQuotes(literal: ts.StringLiteral) { + return literal.getText().substring(1, literal.getText().length - 1); +} + +/** Method that can be used to replace all search occurrences in a string. */ +export function findAll(str: string, search: string): number[] { + const result = []; + let i = -1; + while ((i = str.indexOf(search, i + 1)) !== -1) { + result.push(i); + } + return result; +} + +export function findAllInputsInElWithTag(html: string, name: string, tagNames: string[]): number[] { + return findAllIoInElWithTag(html, name, tagNames, String.raw`\[?`, String.raw`\]?`); +} + +export function findAllOutputsInElWithTag(html: string, name: string, tagNames: string[]): + number[] { + return findAllIoInElWithTag(html, name, tagNames, String.raw`\(`, String.raw`\)`); +} + +/** + * Method that can be used to rename all occurrences of an `@Input()` in a HTML string that occur + * inside an element with any of the given attributes. This is useful for replacing an `@Input()` on + * a `@Directive()` with an attribute selector. + */ +export function findAllInputsInElWithAttr(html: string, name: string, attrs: string[]): number[] { + return findAllIoInElWithAttr(html, name, attrs, String.raw`\[?`, String.raw`\]?`); +} + +/** + * Method that can be used to rename all occurrences of an `@Output()` in a HTML string that occur + * inside an element with any of the given attributes. This is useful for replacing an `@Output()` + * on a `@Directive()` with an attribute selector. + */ +export function findAllOutputsInElWithAttr(html: string, name: string, attrs: string[]): number[] { + return findAllIoInElWithAttr(html, name, attrs, String.raw`\(`, String.raw`\)`); +} + +function findAllIoInElWithTag(html:string, name: string, tagNames: string[], startIoPattern: string, + endIoPattern: string): number[] { + const skipPattern = String.raw`[^>]*\s`; + const openTagPattern = String.raw`<\s*`; + const tagNamesPattern = String.raw`(?:${tagNames.join('|')})`; + const replaceIoPattern = String.raw` + (${openTagPattern}${tagNamesPattern}\s(?:${skipPattern})?${startIoPattern}) + ${name} + ${endIoPattern}[=\s>]`; + const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g'); + const result = []; + let match; + while (match = replaceIoRegex.exec(html)) { + result.push(match.index + match[1].length); + } + return result; +} + +function findAllIoInElWithAttr(html: string, name: string, attrs: string[], startIoPattern: string, + endIoPattern: string): number[] { + const skipPattern = String.raw`[^>]*\s`; + const openTagPattern = String.raw`<\s*\S`; + const attrsPattern = String.raw`(?:${attrs.join('|')})`; + const inputAfterAttrPattern = String.raw` + (${openTagPattern}${skipPattern}${attrsPattern}[=\s](?:${skipPattern})?${startIoPattern}) + ${name} + ${endIoPattern}[=\s>]`; + const inputBeforeAttrPattern = String.raw` + (${openTagPattern}${skipPattern}${startIoPattern}) + ${name} + ${endIoPattern}[=\s](?:${skipPattern})?${attrsPattern}[=\s>]`; + const replaceIoPattern = String.raw`${inputAfterAttrPattern}|${inputBeforeAttrPattern}`; + const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g'); + const result = []; + let match; + while (match = replaceIoRegex.exec(html)) { + result.push(match.index + (match[1] || match[2]).length); + } + return result; +} diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts new file mode 100644 index 000000000000..7f0c5df5a9e5 --- /dev/null +++ b/src/lib/schematics/update/update.ts @@ -0,0 +1,17 @@ +import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import {TslintFixTask} from '@angular-devkit/schematics/tasks'; +import * as path from 'path'; + +export default function(): Rule { + return (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules'), + rules: { + "switch-identifiers": true, + } + }, { + includes: '*.ts', + silent: false, + })); + }; +} From c7b12a324d6ec8f4b2e79acaf0543ce86268b55c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 10:35:23 -0700 Subject: [PATCH 02/11] address comments --- src/lib/schematics/collection.json | 2 +- src/lib/schematics/update/update.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json index b40490ec45e6..c211ad3ee1a9 100644 --- a/src/lib/schematics/collection.json +++ b/src/lib/schematics/collection.json @@ -10,7 +10,7 @@ "aliases": ["material-shell"] }, "ng-update": { - "description": "Attempts to make fixes to the application to make upgrading from Angular Material 5 to 6 easier", + "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", "factory": "./update/update" }, // Create a dashboard component diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 7f0c5df5a9e5..dac0a44aeedc 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -2,6 +2,7 @@ import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; import {TslintFixTask} from '@angular-devkit/schematics/tasks'; import * as path from 'path'; +/** Entry point for `ng update` from Angular CLI. */ export default function(): Rule { return (_: Tree, context: SchematicContext) => { context.addTask(new TslintFixTask({ From 69876d3b1bad116594d55bab64c488f9feb5d065 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 11:15:06 -0700 Subject: [PATCH 03/11] fix bazel --- BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/BUILD.bazel b/BUILD.bazel index bd18f58e479c..62059b6163bd 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -29,6 +29,7 @@ filegroup( "rxjs", "tsickle", "tslib", + "tslint", "tsutils", "typescript", "zone.js", From d820be567cd890e20456720b028ffdd7a9bd7a04 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 14:04:58 -0700 Subject: [PATCH 04/11] make released version work --- src/lib/schematics/update/rules/switchIdentifiersRule.ts | 3 +++ src/lib/schematics/update/update.ts | 2 +- tools/gulp/tasks/material-release.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/schematics/update/rules/switchIdentifiersRule.ts b/src/lib/schematics/update/rules/switchIdentifiersRule.ts index af59a38b976f..1a592dfa8b89 100644 --- a/src/lib/schematics/update/rules/switchIdentifiersRule.ts +++ b/src/lib/schematics/update/rules/switchIdentifiersRule.ts @@ -26,6 +26,9 @@ export class Rule extends Rules.TypedRule { } export class SwitchIdentifiersWalker extends ProgramAwareRuleWalker { + constructor(sf, opt, prog) { + super(sf, opt, prog); + } /** List of Angular Material declarations inside of the current source file. */ materialDeclarations: ts.Declaration[] = []; diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index dac0a44aeedc..ed40544115f5 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -11,8 +11,8 @@ export default function(): Rule { "switch-identifiers": true, } }, { - includes: '*.ts', silent: false, + tsConfigPath: './tsconfig.json', })); }; } diff --git a/tools/gulp/tasks/material-release.ts b/tools/gulp/tasks/material-release.ts index 6e874d72c9f1..b612b0f4fabd 100644 --- a/tools/gulp/tasks/material-release.ts +++ b/tools/gulp/tasks/material-release.ts @@ -31,7 +31,7 @@ const allScssGlob = join(buildConfig.packagesDir, '**/*.scss'); // Pattern matching schematics files to be copied into the @angular/material package. const schematicsGlobs = [ // File templates and schemas are copied as-is from source. - join(schematicsDir, '**/files/**/*'), + join(schematicsDir, '**/+(data|files)/**/*'), join(schematicsDir, '**/+(schema|collection).json'), // JavaScript files compiled from the TypeScript sources. From 2fdc9bc01616c5cb4c837de0045c47d85ac57cea Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 15:22:31 -0700 Subject: [PATCH 05/11] Add additional rules --- src/lib/schematics/BUILD.bazel | 2 +- .../update/rules/switchPropertyNamesRule.ts | 57 +++++++++++++ ...itchStringLiteralAttributeSelectorsRule.ts | 44 ++++++++++ .../rules/switchStringLiteralCssNamesRule.ts | 46 +++++++++++ ...switchStringLiteralElementSelectorsRule.ts | 44 ++++++++++ .../switchTemplateAttributeSelectorsRule.ts | 70 ++++++++++++++++ .../rules/switchTemplateCssNamesRule.ts | 67 ++++++++++++++++ .../switchTemplateElementSelectorsRule.ts | 69 ++++++++++++++++ .../rules/switchTemplateExportAsNamesRule.ts | 66 +++++++++++++++ .../rules/switchTemplateInputNamesRule.ts | 76 ++++++++++++++++++ .../rules/switchTemplateOutputNamesRule.ts | 80 +++++++++++++++++++ src/lib/schematics/update/update.ts | 16 ++++ 12 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 src/lib/schematics/update/rules/switchPropertyNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchStringLiteralAttributeSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchStringLiteralCssNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchStringLiteralElementSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateAttributeSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateCssNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateElementSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateExportAsNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateInputNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchTemplateOutputNamesRule.ts diff --git a/src/lib/schematics/BUILD.bazel b/src/lib/schematics/BUILD.bazel index 2bb4454db454..87da51d756b1 100644 --- a/src/lib/schematics/BUILD.bazel +++ b/src/lib/schematics/BUILD.bazel @@ -16,6 +16,6 @@ npm_package( name = "npm_package", srcs = [ ":collection.json", - ] + glob(["**/files/**/*", "**/schema.json"]), + ] + glob(["**/files/**/*", "**/data/**/*", "**/schema.json"]), deps = [":schematics"], ) diff --git a/src/lib/schematics/update/rules/switchPropertyNamesRule.ts b/src/lib/schematics/update/rules/switchPropertyNamesRule.ts new file mode 100644 index 000000000000..4a2496322e59 --- /dev/null +++ b/src/lib/schematics/update/rules/switchPropertyNamesRule.ts @@ -0,0 +1,57 @@ +import {bold, green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {propertyNames} from '../material/component-data'; + +/** + * Rule that walks through every property access expression and updates properties that have + * been changed in favor of the new name. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new SwitchPropertyNamesWalker(sourceFile, this.getOptions(), program)); + } +} + +export class SwitchPropertyNamesWalker extends ProgramAwareRuleWalker { + visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { + // Recursively call this method for the expression of the current property expression. + // It can happen that there is a chain of property access expressions. + // For example: "mySortInstance.mdSortChange.subscribe()" + if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); + } + + // TODO(mmalerba): This is prrobably a bad way to get the property host... + // Tokens are: [..., , '.', ], so back up 3. + const propHost = prop.getChildAt(prop.getChildCount() - 3); + + const type = this.getTypeChecker().getTypeAtLocation(propHost); + const typeSymbol = type && type.getSymbol(); + const typeName = typeSymbol && typeSymbol.getName(); + const propertyData = propertyNames.find(name => { + if (prop.name.text === name.replace) { + // TODO(mmalerba): Verify that this type comes from Angular Material like we do in + // `switchIdentifiersRule`. + return !name.whitelist || !!typeName && new Set(name.whitelist.classes).has(typeName); + } + return false; + }); + + if (!propertyData) { + return; + } + + const replacement = this.createReplacement(prop.name.getStart(), + prop.name.getWidth(), propertyData.replaceWith); + + const typeMessage = propertyData.whitelist ? `of class "${bold(typeName || '')}"` : ''; + + this.addFailureAtNode( + prop.name, + `Found deprecated property "${red(propertyData.replace)}" ${typeMessage} which has been` + + ` renamed to "${green(propertyData.replaceWith)}"`, + replacement); + } +} diff --git a/src/lib/schematics/update/rules/switchStringLiteralAttributeSelectorsRule.ts b/src/lib/schematics/update/rules/switchStringLiteralAttributeSelectorsRule.ts new file mode 100644 index 000000000000..690758b4fb6e --- /dev/null +++ b/src/lib/schematics/update/rules/switchStringLiteralAttributeSelectorsRule.ts @@ -0,0 +1,44 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; +import * as ts from 'typescript'; +import {attributeSelectors} from '../material/component-data'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every string literal, which includes the outdated Material name and + * is part of a call expression. Those string literals will be changed to the new name. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStringLiteralAttributeSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStringLiteralAttributeSelectorsWalker extends RuleWalker { + visitStringLiteral(stringLiteral: ts.StringLiteral) { + if (stringLiteral.parent && stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + let stringLiteralText = stringLiteral.getFullText(); + + attributeSelectors.forEach(selector => { + this.createReplacementsForOffsets(stringLiteral, selector, + findAll(stringLiteralText, selector.replace)).forEach(replacement => { + this.addFailureAtNode( + stringLiteral, + `Found deprecated attribute selector "${red('[' + selector.replace + ']')}" which has` + + ` been renamed to "${green('[' + selector.replaceWith + ']')}"`, + replacement); + }); + }); + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStringLiteralCssNamesRule.ts b/src/lib/schematics/update/rules/switchStringLiteralCssNamesRule.ts new file mode 100644 index 000000000000..60555ae6db87 --- /dev/null +++ b/src/lib/schematics/update/rules/switchStringLiteralCssNamesRule.ts @@ -0,0 +1,46 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; +import * as ts from 'typescript'; +import {cssNames} from '../material/component-data'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every string literal, which includes the outdated Material name and + * is part of a call expression. Those string literals will be changed to the new name. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStringLiteralCssNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStringLiteralCssNamesWalker extends RuleWalker { + visitStringLiteral(stringLiteral: ts.StringLiteral) { + if (stringLiteral.parent && stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + let stringLiteralText = stringLiteral.getFullText(); + + cssNames.forEach(name => { + if (!name.whitelist || name.whitelist.strings) { + this.createReplacementsForOffsets(stringLiteral, name, + findAll(stringLiteralText, name.replace)).forEach(replacement => { + this.addFailureAtNode( + stringLiteral, + `Found deprecated CSS class "${red(name.replace)}" which has been renamed to` + + ` "${green(name.replaceWith)}"`, + replacement) + }); + } + }); + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStringLiteralElementSelectorsRule.ts b/src/lib/schematics/update/rules/switchStringLiteralElementSelectorsRule.ts new file mode 100644 index 000000000000..01ac7ae880ca --- /dev/null +++ b/src/lib/schematics/update/rules/switchStringLiteralElementSelectorsRule.ts @@ -0,0 +1,44 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; +import * as ts from 'typescript'; +import {elementSelectors} from '../material/component-data'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every string literal, which includes the outdated Material name and + * is part of a call expression. Those string literals will be changed to the new name. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStringLiteralElementSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStringLiteralElementSelectorsWalker extends RuleWalker { + visitStringLiteral(stringLiteral: ts.StringLiteral) { + if (stringLiteral.parent && stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + let stringLiteralText = stringLiteral.getFullText(); + + elementSelectors.forEach(selector => { + this.createReplacementsForOffsets(stringLiteral, selector, + findAll(stringLiteralText, selector.replace)).forEach(replacement => { + this.addFailureAtNode( + stringLiteral, + `Found deprecated element selector "${red(selector.replace)}" which has been` + + ` renamed to "${green(selector.replaceWith)}"`, + replacement); + }); + }); + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateAttributeSelectorsRule.ts b/src/lib/schematics/update/rules/switchTemplateAttributeSelectorsRule.ts new file mode 100644 index 000000000000..29382d0d3d25 --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateAttributeSelectorsRule.ts @@ -0,0 +1,70 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {attributeSelectors} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchTemplateAttributeSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateAttributeSelectorsWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + attributeSelectors.forEach(selector => { + // Being more aggressive with that replacement here allows us to also handle inline + // style elements. Normally we would check if the selector is surrounded by the HTML tag + // characters. + this.createReplacementsForOffsets(node, selector, findAll(templateContent, selector.replace)) + .forEach(replacement => { + replacements.push({ + message: `Found deprecated attribute selector` + + ` "${red('[' + selector.replace + ']')}" which has been renamed to` + + ` "${green('[' + selector.replaceWith + ']')}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateCssNamesRule.ts b/src/lib/schematics/update/rules/switchTemplateCssNamesRule.ts new file mode 100644 index 000000000000..20268b149b2d --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateCssNamesRule.ts @@ -0,0 +1,67 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {cssNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new SwitchTemplateCaaNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateCaaNamesWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + cssNames.forEach(name => { + if (!name.whitelist || name.whitelist.html) { + this.createReplacementsForOffsets(node, name, findAll(templateContent, name.replace)) + .forEach(replacement => { + replacements.push({ + message: `Found deprecated CSS class "${red(name.replace)}" which has been` + + ` renamed to "${green(name.replaceWith)}"`, + replacement + }); + }); + } + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateElementSelectorsRule.ts b/src/lib/schematics/update/rules/switchTemplateElementSelectorsRule.ts new file mode 100644 index 000000000000..2010e6f05835 --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateElementSelectorsRule.ts @@ -0,0 +1,69 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {elementSelectors} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchTemplateElementSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateElementSelectorsWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + elementSelectors.forEach(selector => { + // Being more aggressive with that replacement here allows us to also handle inline + // style elements. Normally we would check if the selector is surrounded by the HTML tag + // characters. + this.createReplacementsForOffsets(node, selector, findAll(templateContent, selector.replace)) + .forEach(replacement => { + replacements.push({ + message: `Found deprecated element selector "${red(selector.replace)}" which has` + + ` been renamed to "${green(selector.replaceWith)}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateExportAsNamesRule.ts b/src/lib/schematics/update/rules/switchTemplateExportAsNamesRule.ts new file mode 100644 index 000000000000..35ad35bff367 --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateExportAsNamesRule.ts @@ -0,0 +1,66 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {exportAsNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchTemplateExportAsNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateExportAsNamesWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + exportAsNames.forEach(name => { + this.createReplacementsForOffsets(node, name, findAll(templateContent, name.replace)) + .forEach(replacement => { + replacements.push({ + message: `Found deprecated exportAs reference "${red(name.replace)}" which has been` + + ` renamed to "${green(name.replaceWith)}"`, + replacement + }); + }) + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateInputNamesRule.ts b/src/lib/schematics/update/rules/switchTemplateInputNamesRule.ts new file mode 100644 index 000000000000..7a0693e7c3e4 --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateInputNamesRule.ts @@ -0,0 +1,76 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {inputNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll, findAllInputsInElWithAttr, findAllInputsInElWithTag} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new SwitchTemplateInputNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateInputNamesWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + inputNames.forEach(name => { + let offsets: number[] = []; + if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) { + offsets = offsets.concat(findAllInputsInElWithAttr( + templateContent, name.replace, name.whitelist.attributes)); + } + if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) { + offsets = offsets.concat(findAllInputsInElWithTag( + templateContent, name.replace, name.whitelist.elements)); + } + if (!name.whitelist) { + offsets = offsets.concat(findAll(templateContent, name.replace)); + } + this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => { + replacements.push({ + message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed to` + + ` "${green(name.replaceWith)}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchTemplateOutputNamesRule.ts b/src/lib/schematics/update/rules/switchTemplateOutputNamesRule.ts new file mode 100644 index 000000000000..e5c0a3bd9f67 --- /dev/null +++ b/src/lib/schematics/update/rules/switchTemplateOutputNamesRule.ts @@ -0,0 +1,80 @@ +import {green, red} from 'chalk'; +import {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {outputNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import { + findAll, + findAllOutputsInElWithAttr, + findAllOutputsInElWithTag +} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new SwitchTemplateOutputNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchTemplateOutputNamesWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private replaceNamesInTemplate(node: ts.Node, templateContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + outputNames.forEach(name => { + let offsets: number[] = []; + if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) { + offsets = offsets.concat(findAllOutputsInElWithAttr( + templateContent, name.replace, name.whitelist.attributes)); + } + if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) { + offsets = offsets.concat(findAllOutputsInElWithTag( + templateContent, name.replace, name.whitelist.elements)); + } + if (!name.whitelist) { + offsets = offsets.concat(findAll(templateContent, name.replace)); + } + this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => { + replacements.push({ + message: `Found deprecated @Output() "${red(name.replace)}" which has been renamed to` + + ` "${green(name.replaceWith)}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index ed40544115f5..66fde4a084a8 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -9,6 +9,22 @@ export default function(): Rule { rulesDirectory: path.join(__dirname, 'rules'), rules: { "switch-identifiers": true, + "switch-property-names": true, + "switch-string-literal-attribute-selectors": true, + "switch-string-literal-css-names": true, + "switch-string-literal-element-selectors": true, + // TODO(mmalerba): These require an extra CLI param, figure out how to handle. + /*"switch-stylesheet-attribute-selectors": true, + "switch-stylesheet-css-names": true, + "switch-stylesheet-element-selectors": true, + "switch-stylesheet-input-names": true, + "switch-stylesheet-output-names": true,*/ + "switch-template-attribute-selectors": true, + "switch-template-css-names": true, + "switch-template-element-selectors": true, + "switch-template-export-as-names": true, + "switch-template-input-names": true, + "switch-template-output-names": true, } }, { silent: false, From 17674271eb79cdcda594288b25fc16f4683d59eb Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 6 Apr 2018 16:52:58 -0700 Subject: [PATCH 06/11] wip --- src/lib/schematics/update/update.ts | 84 +++++++++++++++++++---------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 66fde4a084a8..6aaf355c1718 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -1,34 +1,62 @@ -import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; -import {TslintFixTask} from '@angular-devkit/schematics/tasks'; +import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import {NodePackageInstallTask, TslintFixTask} from '@angular-devkit/schematics/tasks'; import * as path from 'path'; +// Import everything that the `TslintFixTask` will need to cache it in memory. +import * as color from './material/color'; +import * as componentData from './material/component-data'; +import * as extraStylesheets from './material/extra-stylsheets'; +import * as typescriptSpecifiers from './material/typescript-specifiers'; +import * as switchIdentifiersRule from './rules/switchIdentifiersRule'; +import * as switchPropertyNamesRule from './rules/switchPropertyNamesRule'; +import * as switchStringLiteralAttributeSelecotrsRule from './rules/switchStringLiteralAttributeSelectorsRule'; +import * as switchStringLiteralCssNamesRule from './rules/switchStringLiteralCssNamesRule'; +import * as switchStringLiteralElementSelectorsRule from './rules/switchStringLiteralElementSelectorsRule'; +import * as switchTemplateAttributeSelectorRule from './rules/switchTemplateAttributeSelectorsRule'; +import * as switchTemplateCssNamesRule from './rules/switchTemplateCssNamesRule'; +import * as switchTemplateElementSelectorsRule from './rules/switchTemplateElementSelectorsRule'; +import * as switchTemplateExportAsNamesRule from './rules/switchTemplateExportAsNamesRule'; +import * as switchTemplateInputNamesRule from './rules/switchTemplateInputNamesRule'; +import * as switchTemplateOutpitNamesRule from './rules/switchTemplateOutputNamesRule'; +import * as componentFile from './tslint/component-file'; +import * as componentWalker from './tslint/component-walker'; +import * as findTslintBinary from './tslint/find-tslint-binary'; +import * as identifiers from './typescript/identifiers'; +import * as imports from './typescript/imports'; +import * as literal from './typescript/literal'; + /** Entry point for `ng update` from Angular CLI. */ export default function(): Rule { - return (_: Tree, context: SchematicContext) => { - context.addTask(new TslintFixTask({ - rulesDirectory: path.join(__dirname, 'rules'), - rules: { - "switch-identifiers": true, - "switch-property-names": true, - "switch-string-literal-attribute-selectors": true, - "switch-string-literal-css-names": true, - "switch-string-literal-element-selectors": true, - // TODO(mmalerba): These require an extra CLI param, figure out how to handle. - /*"switch-stylesheet-attribute-selectors": true, - "switch-stylesheet-css-names": true, - "switch-stylesheet-element-selectors": true, - "switch-stylesheet-input-names": true, - "switch-stylesheet-output-names": true,*/ - "switch-template-attribute-selectors": true, - "switch-template-css-names": true, - "switch-template-element-selectors": true, - "switch-template-export-as-names": true, - "switch-template-input-names": true, - "switch-template-output-names": true, + return chain([ + (_: Tree, context: SchematicContext) => { + context.addTask(new NodePackageInstallTask()); + }, + (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules'), + rules: { + "switch-identifiers": true, + "switch-property-names": true, + "switch-string-literal-attribute-selectors": true, + "switch-string-literal-css-names": true, + "switch-string-literal-element-selectors": true, + // TODO(mmalerba): These require an extra CLI param, figure out how to handle. + /*"switch-stylesheet-attribute-selectors": true, + "switch-stylesheet-css-names": true, + "switch-stylesheet-element-selectors": true, + "switch-stylesheet-input-names": true, + "switch-stylesheet-output-names": true,*/ + "switch-template-attribute-selectors": true, + "switch-template-css-names": true, + "switch-template-element-selectors": true, + "switch-template-export-as-names": true, + "switch-template-input-names": true, + "switch-template-output-names": true, + } + }, { + silent: false, + tsConfigPath: './tsconfig.json', + })); } - }, { - silent: false, - tsConfigPath: './tsconfig.json', - })); - }; + ]); } From fa6ec090748351bfe189624dfe27af8ac708ca33 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 9 Apr 2018 10:05:23 -0700 Subject: [PATCH 07/11] wip --- package.json | 2 +- src/lib/schematics/update/update.ts | 73 +++++++++++++++++------------ 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 7918bf12e680..dee0541e7290 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@angular-devkit/core": "^0.5.4", - "@angular-devkit/schematics": "^0.5.4", + "@angular-devkit/schematics": "file:../devkit/dist/@angular-devkit/schematics/angular-devkit-schematics-0.5.4.tgz", "@angular/bazel": "6.0.0-rc.3", "@angular/compiler-cli": "6.0.0-rc.3", "@angular/http": "6.0.0-rc.3", diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 6aaf355c1718..5ab05e44d7eb 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -28,35 +28,48 @@ import * as literal from './typescript/literal'; /** Entry point for `ng update` from Angular CLI. */ export default function(): Rule { return chain([ - (_: Tree, context: SchematicContext) => { - context.addTask(new NodePackageInstallTask()); - }, - (_: Tree, context: SchematicContext) => { - context.addTask(new TslintFixTask({ - rulesDirectory: path.join(__dirname, 'rules'), - rules: { - "switch-identifiers": true, - "switch-property-names": true, - "switch-string-literal-attribute-selectors": true, - "switch-string-literal-css-names": true, - "switch-string-literal-element-selectors": true, - // TODO(mmalerba): These require an extra CLI param, figure out how to handle. - /*"switch-stylesheet-attribute-selectors": true, - "switch-stylesheet-css-names": true, - "switch-stylesheet-element-selectors": true, - "switch-stylesheet-input-names": true, - "switch-stylesheet-output-names": true,*/ - "switch-template-attribute-selectors": true, - "switch-template-css-names": true, - "switch-template-element-selectors": true, - "switch-template-export-as-names": true, - "switch-template-input-names": true, - "switch-template-output-names": true, - } - }, { - silent: false, - tsConfigPath: './tsconfig.json', - })); - } + (_: Tree, context: SchematicContext) => { + context.addTask(new NodePackageInstallTask({ + packageName: '@angular/cdk@">=5 <6"' + })); + context.addTask(new NodePackageInstallTask({ + packageName: '@angular/material@">=5 <6"' + })); + }, + (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules'), + rules: { + "switch-identifiers": true, + "switch-property-names": true, + "switch-string-literal-attribute-selectors": true, + "switch-string-literal-css-names": true, + "switch-string-literal-element-selectors": true, + // TODO(mmalerba): These require an extra CLI param, figure out how to handle. + /*"switch-stylesheet-attribute-selectors": true, + "switch-stylesheet-css-names": true, + "switch-stylesheet-element-selectors": true, + "switch-stylesheet-input-names": true, + "switch-stylesheet-output-names": true,*/ + "switch-template-attribute-selectors": true, + "switch-template-css-names": true, + "switch-template-element-selectors": true, + "switch-template-export-as-names": true, + "switch-template-input-names": true, + "switch-template-output-names": true, + } + }, { + silent: false, + tsConfigPath: './tsconfig.json', + })); + }, + (_: Tree, context: SchematicContext) => { + context.addTask(new NodePackageInstallTask({ + packageName: '@angular/cdk@next' + })); + context.addTask(new NodePackageInstallTask({ + packageName: '@angular/material@next' + })); + }, ]); } From 1b66ef525e75b9f50580d7382dc669ec0e706d27 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 9 Apr 2018 17:13:40 -0700 Subject: [PATCH 08/11] yay, working --- src/lib/schematics/collection.json | 4 + src/lib/schematics/update/update.ts | 147 +++++++++++++++------------- 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json index c211ad3ee1a9..57d48bcb7fc8 100644 --- a/src/lib/schematics/collection.json +++ b/src/lib/schematics/collection.json @@ -13,6 +13,10 @@ "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", "factory": "./update/update" }, + "ng-post-update": { + "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", + "factory": "./update/update#postUpdate", + }, // Create a dashboard component "materialDashboard": { "description": "Create a card-based dashboard component", diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 5ab05e44d7eb..0073107e52b6 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -1,75 +1,84 @@ -import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; -import {NodePackageInstallTask, TslintFixTask} from '@angular-devkit/schematics/tasks'; +import {FileEntry, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import { + NodePackageInstallTask, + RunSchematicTask, + TslintFixTask +} from '@angular-devkit/schematics/tasks'; import * as path from 'path'; -// Import everything that the `TslintFixTask` will need to cache it in memory. -import * as color from './material/color'; -import * as componentData from './material/component-data'; -import * as extraStylesheets from './material/extra-stylsheets'; -import * as typescriptSpecifiers from './material/typescript-specifiers'; -import * as switchIdentifiersRule from './rules/switchIdentifiersRule'; -import * as switchPropertyNamesRule from './rules/switchPropertyNamesRule'; -import * as switchStringLiteralAttributeSelecotrsRule from './rules/switchStringLiteralAttributeSelectorsRule'; -import * as switchStringLiteralCssNamesRule from './rules/switchStringLiteralCssNamesRule'; -import * as switchStringLiteralElementSelectorsRule from './rules/switchStringLiteralElementSelectorsRule'; -import * as switchTemplateAttributeSelectorRule from './rules/switchTemplateAttributeSelectorsRule'; -import * as switchTemplateCssNamesRule from './rules/switchTemplateCssNamesRule'; -import * as switchTemplateElementSelectorsRule from './rules/switchTemplateElementSelectorsRule'; -import * as switchTemplateExportAsNamesRule from './rules/switchTemplateExportAsNamesRule'; -import * as switchTemplateInputNamesRule from './rules/switchTemplateInputNamesRule'; -import * as switchTemplateOutpitNamesRule from './rules/switchTemplateOutputNamesRule'; -import * as componentFile from './tslint/component-file'; -import * as componentWalker from './tslint/component-walker'; -import * as findTslintBinary from './tslint/find-tslint-binary'; -import * as identifiers from './typescript/identifiers'; -import * as imports from './typescript/imports'; -import * as literal from './typescript/literal'; +const schematicsSrcPath = 'node_modules/@angular/material/schematics'; +const schematicsTmpPath = 'node_modules/_tmp_angular_material_schematics'; /** Entry point for `ng update` from Angular CLI. */ export default function(): Rule { - return chain([ - (_: Tree, context: SchematicContext) => { - context.addTask(new NodePackageInstallTask({ - packageName: '@angular/cdk@">=5 <6"' - })); - context.addTask(new NodePackageInstallTask({ - packageName: '@angular/material@">=5 <6"' - })); - }, - (_: Tree, context: SchematicContext) => { - context.addTask(new TslintFixTask({ - rulesDirectory: path.join(__dirname, 'rules'), - rules: { - "switch-identifiers": true, - "switch-property-names": true, - "switch-string-literal-attribute-selectors": true, - "switch-string-literal-css-names": true, - "switch-string-literal-element-selectors": true, - // TODO(mmalerba): These require an extra CLI param, figure out how to handle. - /*"switch-stylesheet-attribute-selectors": true, - "switch-stylesheet-css-names": true, - "switch-stylesheet-element-selectors": true, - "switch-stylesheet-input-names": true, - "switch-stylesheet-output-names": true,*/ - "switch-template-attribute-selectors": true, - "switch-template-css-names": true, - "switch-template-element-selectors": true, - "switch-template-export-as-names": true, - "switch-template-input-names": true, - "switch-template-output-names": true, - } - }, { - silent: false, - tsConfigPath: './tsconfig.json', - })); - }, - (_: Tree, context: SchematicContext) => { - context.addTask(new NodePackageInstallTask({ - packageName: '@angular/cdk@next' - })); - context.addTask(new NodePackageInstallTask({ - packageName: '@angular/material@next' - })); - }, - ]); + return (tree: Tree, context: SchematicContext) => { + // Copy the update schematics to a temporary directory. + const updateSrcs: FileEntry[] = []; + tree.getDir(schematicsSrcPath).visit((_, entry) => updateSrcs.push(entry)); + for (let src of updateSrcs) { + tree.create(src.path.replace(schematicsSrcPath, schematicsTmpPath), src.content); + } + + // Downgrade @angular/material to 5.x. This allows us to use the 5.x type information in the + // update script. + const downgradeCdkTask = context.addTask(new NodePackageInstallTask({ + packageName: '@angular/cdk@">=5 <6"' + })); + const downgradeMaterialTask = context.addTask(new NodePackageInstallTask({ + packageName: '@angular/material@">=5 <6"' + })); + + // Run the update tslint rules. + const updateTask = context.addTask(new TslintFixTask({ + rulesDirectory: path.join(schematicsTmpPath, 'update/rules'), + rules: { + "switch-identifiers": true, + "switch-property-names": true, + "switch-string-literal-attribute-selectors": true, + "switch-string-literal-css-names": true, + "switch-string-literal-element-selectors": true, + // TODO(mmalerba): These require an extra CLI param, figure out how to handle. + /*"switch-stylesheet-attribute-selectors": true, + "switch-stylesheet-css-names": true, + "switch-stylesheet-element-selectors": true, + "switch-stylesheet-input-names": true, + "switch-stylesheet-output-names": true,*/ + "switch-template-attribute-selectors": true, + "switch-template-css-names": true, + "switch-template-element-selectors": true, + "switch-template-export-as-names": true, + "switch-template-input-names": true, + "switch-template-output-names": true, + } + }, { + silent: false, + tsConfigPath: './tsconfig.json', + }), [downgradeCdkTask, downgradeMaterialTask]); + + // Upgrade @angular/material back to 6.x. + const upgradeCdkTask = context.addTask(new NodePackageInstallTask({ + // TODO(mmalerba): Change "next" to ">=6 <7". + packageName: '@angular/cdk@next' + }), [updateTask]); + const upgradeMaterialTask = context.addTask(new NodePackageInstallTask({ + // TODO(mmalerba): Fix before submitting. + packageName: '/usr/local/google/home/mmalerba/material2/dist/releases/material/angular-material-6.0.0-rc.1.tgz' + }), [updateTask]); + + // Delete the temporary schematics directory. + context.addTask( + new RunSchematicTask(path.join(__dirname, '../collection.json'), 'ng-post-update', { + deleteFiles: updateSrcs + .map(entry => entry.path.replace(schematicsSrcPath, schematicsTmpPath)) + }), [upgradeCdkTask, upgradeMaterialTask]); + }; +} + +/** Post-update schematic to be called when ng update is finished. */ +export function postUpdate(options: {deleteFiles: string[]}): Rule { + return (tree: Tree) => { + for (let file of options.deleteFiles) { + tree.delete(file); + } + } } From 7d864b84afc980bb60ca6a5d199d81b63dced3d2 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 10 Apr 2018 10:11:52 -0700 Subject: [PATCH 09/11] all rules migrated --- src/lib/schematics/collection.json | 2 + .../rules/checkClassDeclarationMiscRule.ts | 36 ++++++++ .../update/rules/checkIdentifierMiscRule.ts | 32 ++++++++ .../update/rules/checkImportMiscRule.ts | 29 +++++++ .../update/rules/checkInheritanceRule.ts | 35 ++++++++ .../update/rules/checkMethodCallsRule.ts | 82 +++++++++++++++++++ .../rules/checkPropertyAccessMiscRule.ts | 52 ++++++++++++ .../update/rules/checkTemplateMiscRule.ts | 80 ++++++++++++++++++ .../switchStylesheetAttributeSelectorsRule.ts | 82 +++++++++++++++++++ .../rules/switchStylesheetCssNamesRule.ts | 79 ++++++++++++++++++ .../switchStylesheetElementSelectorsRule.ts | 78 ++++++++++++++++++ .../rules/switchStylesheetInputNamesRule.ts | 81 ++++++++++++++++++ .../rules/switchStylesheetOutputNamesRule.ts | 81 ++++++++++++++++++ src/lib/schematics/update/update.ts | 19 ++++- 14 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 src/lib/schematics/update/rules/checkClassDeclarationMiscRule.ts create mode 100644 src/lib/schematics/update/rules/checkIdentifierMiscRule.ts create mode 100644 src/lib/schematics/update/rules/checkImportMiscRule.ts create mode 100644 src/lib/schematics/update/rules/checkInheritanceRule.ts create mode 100644 src/lib/schematics/update/rules/checkMethodCallsRule.ts create mode 100644 src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts create mode 100644 src/lib/schematics/update/rules/checkTemplateMiscRule.ts create mode 100644 src/lib/schematics/update/rules/switchStylesheetAttributeSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchStylesheetCssNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchStylesheetElementSelectorsRule.ts create mode 100644 src/lib/schematics/update/rules/switchStylesheetInputNamesRule.ts create mode 100644 src/lib/schematics/update/rules/switchStylesheetOutputNamesRule.ts diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json index 57d48bcb7fc8..075971d4e10d 100644 --- a/src/lib/schematics/collection.json +++ b/src/lib/schematics/collection.json @@ -16,6 +16,8 @@ "ng-post-update": { "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", "factory": "./update/update#postUpdate", + // TODO(mmalerba): put back. + // "private": true }, // Create a dashboard component "materialDashboard": { diff --git a/src/lib/schematics/update/rules/checkClassDeclarationMiscRule.ts b/src/lib/schematics/update/rules/checkClassDeclarationMiscRule.ts new file mode 100644 index 000000000000..afbb01f342ed --- /dev/null +++ b/src/lib/schematics/update/rules/checkClassDeclarationMiscRule.ts @@ -0,0 +1,36 @@ +import {bold, green} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; + +/** + * Rule that walks through every identifier that is part of Angular Material and replaces the + * outdated name with the new one. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new CheckClassDeclarationMiscWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckClassDeclarationMiscWalker extends ProgramAwareRuleWalker { + visitClassDeclaration(declaration: ts.ClassDeclaration) { + if (declaration.heritageClauses) { + declaration.heritageClauses.forEach(hc => { + const classes = new Set(hc.types.map(t => t.getFirstToken().getText())); + if (classes.has('MatFormFieldControl')) { + const sfl = declaration.members + .filter(prop => prop.getFirstToken().getText() === 'shouldFloatLabel'); + if (!sfl.length && declaration.name) { + this.addFailureAtNode( + declaration, + `Found class "${bold(declaration.name.text)}" which extends` + + ` "${bold('MatFormFieldControl')}". This class must define` + + ` "${green('shouldLabelFloat')}" which is now a required property.` + ) + } + } + }); + } + } +} diff --git a/src/lib/schematics/update/rules/checkIdentifierMiscRule.ts b/src/lib/schematics/update/rules/checkIdentifierMiscRule.ts new file mode 100644 index 000000000000..4a344bb50bcd --- /dev/null +++ b/src/lib/schematics/update/rules/checkIdentifierMiscRule.ts @@ -0,0 +1,32 @@ +import {bold, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; + +/** + * Rule that walks through every identifier that is part of Angular Material and replaces the + * outdated name with the new one. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new CheckIdentifierMiscWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckIdentifierMiscWalker extends ProgramAwareRuleWalker { + visitIdentifier(identifier: ts.Identifier) { + if (identifier.getText() === 'MatDrawerToggleResult') { + this.addFailureAtNode( + identifier, + `Found "${bold('MatDrawerToggleResult')}" which has changed from a class type to a` + + ` string literal type. Code may need to be updated`); + } + + if (identifier.getText() === 'MatListOptionChange') { + this.addFailureAtNode( + identifier, + `Found usage of "${red('MatListOptionChange')}" which has been removed. Please listen` + + ` for ${bold('selectionChange')} on ${bold('MatSelectionList')} instead`); + } + } +} diff --git a/src/lib/schematics/update/rules/checkImportMiscRule.ts b/src/lib/schematics/update/rules/checkImportMiscRule.ts new file mode 100644 index 000000000000..a228462303b7 --- /dev/null +++ b/src/lib/schematics/update/rules/checkImportMiscRule.ts @@ -0,0 +1,29 @@ +import {bold, green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {isMaterialImportDeclaration} from '../material/typescript-specifiers'; + +/** + * Rule that walks through every identifier that is part of Angular Material and replaces the + * outdated name with the new one. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker(new CheckImportMiscWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckImportMiscWalker extends ProgramAwareRuleWalker { + visitImportDeclaration(declaration: ts.ImportDeclaration) { + if (isMaterialImportDeclaration(declaration)) { + declaration.importClause.namedBindings.forEachChild(n => { + let importName = n.getFirstToken() && n.getFirstToken().getText(); + if (importName === 'SHOW_ANIMATION' || importName === 'HIDE_ANIMATION') { + this.addFailureAtNode( + n, + `Found deprecated symbol "${red(importName)}" which has been removed`); + } + }); + } + } +} diff --git a/src/lib/schematics/update/rules/checkInheritanceRule.ts b/src/lib/schematics/update/rules/checkInheritanceRule.ts new file mode 100644 index 000000000000..59122599e1c3 --- /dev/null +++ b/src/lib/schematics/update/rules/checkInheritanceRule.ts @@ -0,0 +1,35 @@ +import {bold, green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {propertyNames} from '../material/component-data'; + +/** + * Rule that walks through every property access expression and updates properties that have + * been changed in favor of the new name. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new CheckInheritanceWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckInheritanceWalker extends ProgramAwareRuleWalker { + visitClassDeclaration(declaration: ts.ClassDeclaration) { + // Check if user is extending an Angular Material class whose properties have changed. + const type = this.getTypeChecker().getTypeAtLocation(declaration.name); + const baseTypes = this.getTypeChecker().getBaseTypes(type as ts.InterfaceType); + baseTypes.forEach(t => { + const propertyData = propertyNames.find( + data => data.whitelist && new Set(data.whitelist.classes).has(t.symbol.name)); + if (propertyData) { + this.addFailureAtNode( + declaration, + `Found class "${bold(declaration.name.text)}" which extends class` + + ` "${bold(t.symbol.name)}". Please note that the base class property` + + ` "${red(propertyData.replace)}" has changed to "${green(propertyData.replaceWith)}".` + + ` You may need to update your class as well`); + } + }); + } +} diff --git a/src/lib/schematics/update/rules/checkMethodCallsRule.ts b/src/lib/schematics/update/rules/checkMethodCallsRule.ts new file mode 100644 index 000000000000..29e04384210a --- /dev/null +++ b/src/lib/schematics/update/rules/checkMethodCallsRule.ts @@ -0,0 +1,82 @@ +import {bold} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {color} from '../material/color'; +import {methodCallChecks} from '../material/component-data'; + +/** + * Rule that walks through every property access expression and updates properties that have + * been changed in favor of the new name. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new CheckMethodCallsWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckMethodCallsWalker extends ProgramAwareRuleWalker { + visitNewExpression(expression: ts.NewExpression) { + const symbol = this.getTypeChecker().getTypeAtLocation(expression).symbol; + if (symbol) { + const className = symbol.name; + this.checkConstructor(expression, className); + } + } + + visitCallExpression(expression: ts.CallExpression) { + if (expression.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { + const methodName = expression.getFirstToken().getText(); + + if (methodName === 'super') { + const type = this.getTypeChecker().getTypeAtLocation(expression.expression); + const className = type.symbol && type.symbol.name; + if (className) { + this.checkConstructor(expression, className); + } + } + return; + } + + // TODO(mmalerba): This is probably a bad way to get the class node... + // Tokens are: [..., , '.', ], so back up 3. + const accessExp = expression.expression; + const classNode = accessExp.getChildAt(accessExp.getChildCount() - 3); + const methodNode = accessExp.getChildAt(accessExp.getChildCount() - 1); + const methodName = methodNode.getText(); + const type = this.getTypeChecker().getTypeAtLocation(classNode); + const className = type.symbol && type.symbol.name; + + const currentCheck = methodCallChecks + .find(data => data.method === methodName && data.className === className); + if (!currentCheck) { + return; + } + + const failure = currentCheck.invalidArgCounts + .find(countData => countData.count === expression.arguments.length); + if (failure) { + this.addFailureAtNode( + expression, + `Found call to "${bold(className + '.' + methodName)}" with` + + ` ${bold(String(failure.count))} arguments. ${color(failure.message)}`); + } + } + + private checkConstructor(node: ts.NewExpression | ts.CallExpression, className: string) { + const currentCheck = methodCallChecks + .find(data => data.method === 'constructor' && data.className === className); + if (!currentCheck) { + return; + } + + const failure = currentCheck.invalidArgCounts + .find(countData => !!node.arguments && countData.count === node.arguments.length); + if (failure) { + this.addFailureAtNode( + node, + `Found "${bold(className)}" constructed with ${bold(String(failure.count))} arguments.` + + ` ${color(failure.message)}`); + } + } +} diff --git a/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts b/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts new file mode 100644 index 000000000000..84415e494d78 --- /dev/null +++ b/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts @@ -0,0 +1,52 @@ +import {bold, green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; + +/** + * Rule that walks through every identifier that is part of Angular Material and replaces the + * outdated name with the new one. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker( + new CheckPropertyAccessMiscWalker(sourceFile, this.getOptions(), program)); + } +} + +export class CheckPropertyAccessMiscWalker extends ProgramAwareRuleWalker { + visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { + // Recursively call this method for the expression of the current property expression. + // It can happen that there is a chain of property access expressions. + // For example: "mySortInstance.mdSortChange.subscribe()" + if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); + } + + // TODO(mmalerba): This is probably a bad way to get the property host... + // Tokens are: [..., , '.', ], so back up 3. + const propHost = prop.getChildAt(prop.getChildCount() - 3); + + const type = this.getTypeChecker().getTypeAtLocation(propHost); + const typeSymbol = type && type.getSymbol(); + + if (typeSymbol) { + const typeName = typeSymbol.getName(); + + if (typeName === 'MatListOption' && prop.name.text === 'selectionChange') { + this.addFailureAtNode( + prop, + `Found deprecated property "${red('selectionChange')}" of class` + + ` "${bold('MatListOption')}". Use the "${green('selectionChange')}" property on the` + + ` parent "${bold('MatSelectionList')}" instead.`); + } + + if (typeName === 'MatDatepicker' && prop.name.text === 'selectedChanged') { + this.addFailureAtNode( + prop, + `Found deprecated property "${red('selectedChanged')}" of class` + + ` "${bold('MatDatepicker')}". Use the "${green('dateChange')}" or` + + ` "${green('dateInput')}" methods on "${bold('MatDatepickerInput')}" instead`); + } + } + } +} diff --git a/src/lib/schematics/update/rules/checkTemplateMiscRule.ts b/src/lib/schematics/update/rules/checkTemplateMiscRule.ts new file mode 100644 index 000000000000..f6c916aec564 --- /dev/null +++ b/src/lib/schematics/update/rules/checkTemplateMiscRule.ts @@ -0,0 +1,80 @@ +import {bold, green, red} from 'chalk'; +import {RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll, findAllInputsInElWithTag, findAllOutputsInElWithTag} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * templates. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new CheckTemplateMiscWalker(sourceFile, this.getOptions())); + } +} + +export class CheckTemplateMiscWalker extends ComponentWalker { + visitInlineTemplate(template: ts.StringLiteral) { + this.checkTemplate(template.getText()).forEach(failure => { + const ruleFailure = new RuleFailure(template.getSourceFile(), failure.start, failure.end, + failure.message, this.getRuleName()); + this.addFailure(ruleFailure); + }); + } + + visitExternalTemplate(template: ExternalResource) { + this.checkTemplate(template.getFullText()).forEach(failure => { + const ruleFailure = new RuleFailure(template, failure.start + 1, failure.end + 1, + failure.message, this.getRuleName()); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the template with the new one and returns an updated template. + */ + private checkTemplate(templateContent: string): + {start: number, end: number, message: string}[] { + let failures: {message: string, start: number, end: number}[] = []; + + failures = failures.concat(findAll(templateContent, 'cdk-focus-trap').map(offset => ({ + start: offset, + end: offset + 'cdk-focus-trap'.length, + message: `Found deprecated element selector "${red('cdk-focus-trap')}" which has been` + + ` changed to an attribute selector "${green('[cdkTrapFocus]')}"` + }))); + + failures = failures.concat( + findAllOutputsInElWithTag(templateContent, 'selectionChange', ['mat-list-option']) + .map(offset => ({ + start: offset, + end: offset + 'selectionChange'.length, + message: `Found deprecated @Output() "${red('selectionChange')}" on` + + ` "${bold('mat-list-option')}". Use "${green('selectionChange')}" on` + + ` "${bold('mat-selection-list')}" instead` + }))); + + failures = failures.concat( + findAllOutputsInElWithTag(templateContent, 'selectedChanged', ['mat-datepicker']) + .map(offset => ({ + start: offset, + end: offset + 'selectionChange'.length, + message: `Found deprecated @Output() "${red('selectedChanged')}" on` + + ` "${bold('mat-datepicker')}". Use "${green('dateChange')}" or` + + ` "${green('dateInput')}" on "${bold('')}" instead` + }))); + + failures = failures.concat( + findAllInputsInElWithTag(templateContent, 'selected', ['mat-button-toggle-group']) + .map(offset => ({ + start: offset, + end: offset + 'selected'.length, + message: `Found deprecated @Input() "${red('selected')}" on`+ + ` "${bold('mat-radio-button-group')}". Use "${green('value')}" instead` + }))); + + return failures; + } +} diff --git a/src/lib/schematics/update/rules/switchStylesheetAttributeSelectorsRule.ts b/src/lib/schematics/update/rules/switchStylesheetAttributeSelectorsRule.ts new file mode 100644 index 000000000000..d203ef6ea523 --- /dev/null +++ b/src/lib/schematics/update/rules/switchStylesheetAttributeSelectorsRule.ts @@ -0,0 +1,82 @@ +import {green, red} from 'chalk'; +import {sync as globSync} from 'glob'; +import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {attributeSelectors} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * stylesheets. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStylesheetAtributeSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStylesheetAtributeSelectorsWalker extends ComponentWalker { + + constructor(sourceFile: ts.SourceFile, options: IOptions) { + // In some applications, developers will have global stylesheets that are not specified in any + // Angular component. Therefore we glob up all css and scss files outside of node_modules and + // dist and check them as well. + const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)'); + super(sourceFile, options, extraFiles); + extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); + } + + visitInlineStylesheet(stylesheet: ts.StringLiteral) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalStylesheet(stylesheet: ExternalResource) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the stylesheet with the new one and returns an updated + * stylesheet. + */ + private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + attributeSelectors.forEach(selector => { + const bracketedSelector = { + replace: `[${selector.replace}]`, + replaceWith: `[${selector.replaceWith}]` + }; + this.createReplacementsForOffsets(node, bracketedSelector, + findAll(stylesheetContent, bracketedSelector.replace)).forEach(replacement => { + replacements.push({ + message: `Found deprecated attribute selector "${red(bracketedSelector.replace)}"` + + ` which has been renamed to "${green(bracketedSelector.replaceWith)}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStylesheetCssNamesRule.ts b/src/lib/schematics/update/rules/switchStylesheetCssNamesRule.ts new file mode 100644 index 000000000000..04afb5ba7ae2 --- /dev/null +++ b/src/lib/schematics/update/rules/switchStylesheetCssNamesRule.ts @@ -0,0 +1,79 @@ +import {green, red} from 'chalk'; +import {sync as globSync} from 'glob'; +import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {cssNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * stylesheets. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker(new SwitchStylesheetCssNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStylesheetCssNamesWalker extends ComponentWalker { + + constructor(sourceFile: ts.SourceFile, options: IOptions) { + // In some applications, developers will have global stylesheets that are not specified in any + // Angular component. Therefore we glob up all css and scss files outside of node_modules and + // dist and check them as well. + const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)'); + super(sourceFile, options, extraFiles); + extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); + } + + visitInlineStylesheet(stylesheet: ts.StringLiteral) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalStylesheet(stylesheet: ExternalResource) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the stylesheet with the new one and returns an updated + * stylesheet. + */ + private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + cssNames.forEach(name => { + if (!name.whitelist || name.whitelist.css) { + this.createReplacementsForOffsets(node, name, findAll(stylesheetContent, name.replace)) + .forEach(replacement => { + replacements.push({ + message: `Found CSS class "${red(name.replace)}" which has been renamed to` + + ` "${green(name.replaceWith)}"`, + replacement + }); + }); + } + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStylesheetElementSelectorsRule.ts b/src/lib/schematics/update/rules/switchStylesheetElementSelectorsRule.ts new file mode 100644 index 000000000000..7515aaa85522 --- /dev/null +++ b/src/lib/schematics/update/rules/switchStylesheetElementSelectorsRule.ts @@ -0,0 +1,78 @@ +import {green, red} from 'chalk'; +import {sync as globSync} from 'glob'; +import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {elementSelectors} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * stylesheets. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStylesheetElementSelectorsWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStylesheetElementSelectorsWalker extends ComponentWalker { + + constructor(sourceFile: ts.SourceFile, options: IOptions) { + // In some applications, developers will have global stylesheets that are not specified in any + // Angular component. Therefore we glob up all css and scss files outside of node_modules and + // dist and check them as well. + const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)'); + super(sourceFile, options, extraFiles); + extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); + } + + visitInlineStylesheet(stylesheet: ts.StringLiteral) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalStylesheet(stylesheet: ExternalResource) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the stylesheet with the new one and returns an updated + * stylesheet. + */ + private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + elementSelectors.forEach(selector => { + this.createReplacementsForOffsets(node, selector, + findAll(stylesheetContent, selector.replace)).forEach(replacement => { + replacements.push({ + message: `Found deprecated element selector "${red(selector.replace)}" which has` + + ` been renamed to "${green(selector.replaceWith)}"`, + replacement + }); + }); + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStylesheetInputNamesRule.ts b/src/lib/schematics/update/rules/switchStylesheetInputNamesRule.ts new file mode 100644 index 000000000000..826fa2349784 --- /dev/null +++ b/src/lib/schematics/update/rules/switchStylesheetInputNamesRule.ts @@ -0,0 +1,81 @@ +import {green, red} from 'chalk'; +import {sync as globSync} from 'glob'; +import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {inputNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * stylesheets. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStylesheetInputNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStylesheetInputNamesWalker extends ComponentWalker { + + constructor(sourceFile: ts.SourceFile, options: IOptions) { + // In some applications, developers will have global stylesheets that are not specified in any + // Angular component. Therefore we glob up all css and scss files outside of node_modules and + // dist and check them as well. + const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)'); + super(sourceFile, options, extraFiles); + extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); + } + + visitInlineStylesheet(stylesheet: ts.StringLiteral) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalStylesheet(stylesheet: ExternalResource) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the stylesheet with the new one and returns an updated + * stylesheet. + */ + private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + inputNames.forEach(name => { + if (!name.whitelist || name.whitelist.css) { + const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`}; + this.createReplacementsForOffsets(node, name, + findAll(stylesheetContent, bracketedName.replace)).forEach(replacement => { + replacements.push({ + message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed` + + ` to "${green(name.replaceWith)}"`, + replacement + }); + }); + } + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/rules/switchStylesheetOutputNamesRule.ts b/src/lib/schematics/update/rules/switchStylesheetOutputNamesRule.ts new file mode 100644 index 000000000000..33bca8311b4b --- /dev/null +++ b/src/lib/schematics/update/rules/switchStylesheetOutputNamesRule.ts @@ -0,0 +1,81 @@ +import {green, red} from 'chalk'; +import {sync as globSync} from 'glob'; +import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {outputNames} from '../material/component-data'; +import {ExternalResource} from '../tslint/component-file'; +import {ComponentWalker} from '../tslint/component-walker'; +import {findAll} from '../typescript/literal'; + +/** + * Rule that walks through every component decorator and updates their inline or external + * stylesheets. + */ +export class Rule extends Rules.AbstractRule { + apply(sourceFile: ts.SourceFile): RuleFailure[] { + return this.applyWithWalker( + new SwitchStylesheetOutputNamesWalker(sourceFile, this.getOptions())); + } +} + +export class SwitchStylesheetOutputNamesWalker extends ComponentWalker { + + constructor(sourceFile: ts.SourceFile, options: IOptions) { + // In some applications, developers will have global stylesheets that are not specified in any + // Angular component. Therefore we glob up all css and scss files outside of node_modules and + // dist and check them as well. + const extraFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)'); + super(sourceFile, options, extraFiles); + extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); + } + + visitInlineStylesheet(stylesheet: ts.StringLiteral) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + visitExternalStylesheet(stylesheet: ExternalResource) { + this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { + const fix = replacement.replacement; + const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, + replacement.message, this.getRuleName(), fix); + this.addFailure(ruleFailure); + }); + } + + /** + * Replaces the outdated name in the stylesheet with the new one and returns an updated + * stylesheet. + */ + private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): + {message: string, replacement: Replacement}[] { + const replacements: {message: string, replacement: Replacement}[] = []; + + outputNames.forEach(name => { + if (!name.whitelist || name.whitelist.css) { + const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`}; + this.createReplacementsForOffsets(node, name, + findAll(stylesheetContent, bracketedName.replace)).forEach(replacement => { + replacements.push({ + message: `Found deprecated @Output() "${red(name.replace)}" which has been` + + ` renamed to "${green(name.replaceWith)}"`, + replacement + }); + }); + } + }); + + return replacements; + } + + private createReplacementsForOffsets(node: ts.Node, + update: {replace: string, replaceWith: string}, + offsets: number[]): Replacement[] { + return offsets.map(offset => this.createReplacement( + node.getStart() + offset, update.replace.length, update.replaceWith)); + } +} diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 0073107e52b6..c389b76c151a 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -32,26 +32,36 @@ export default function(): Rule { const updateTask = context.addTask(new TslintFixTask({ rulesDirectory: path.join(schematicsTmpPath, 'update/rules'), rules: { + // Automatic fixes. "switch-identifiers": true, "switch-property-names": true, "switch-string-literal-attribute-selectors": true, "switch-string-literal-css-names": true, "switch-string-literal-element-selectors": true, - // TODO(mmalerba): These require an extra CLI param, figure out how to handle. - /*"switch-stylesheet-attribute-selectors": true, + "switch-stylesheet-attribute-selectors": true, "switch-stylesheet-css-names": true, "switch-stylesheet-element-selectors": true, "switch-stylesheet-input-names": true, - "switch-stylesheet-output-names": true,*/ + "switch-stylesheet-output-names": true, "switch-template-attribute-selectors": true, "switch-template-css-names": true, "switch-template-element-selectors": true, "switch-template-export-as-names": true, "switch-template-input-names": true, "switch-template-output-names": true, + + // Additional issues we can detect but not automatically fix. + "check-class-declaration-misc": true, + "check-identifier-misc": true, + "check-import-misc": true, + "check-inheritance": true, + "check-method-calls": true, + "check-property-access-misc": true, + "check-template-misc": true } }, { silent: false, + ignoreErrors: true, tsConfigPath: './tsconfig.json', }), [downgradeCdkTask, downgradeMaterialTask]); @@ -80,5 +90,8 @@ export function postUpdate(options: {deleteFiles: string[]}): Rule { for (let file of options.deleteFiles) { tree.delete(file); } + + console.log('\nComplete! Please check the output above for any issues that were detected but' + + ' could not be automatically fixed.'); } } From c4e526e6a8117f01a8efb1d7512e7ac6ce0c2cba Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 10 Apr 2018 10:30:01 -0700 Subject: [PATCH 10/11] combine packages in single `npm i` call --- src/lib/schematics/update/update.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index c389b76c151a..2880888c4fcb 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -19,13 +19,10 @@ export default function(): Rule { tree.create(src.path.replace(schematicsSrcPath, schematicsTmpPath), src.content); } - // Downgrade @angular/material to 5.x. This allows us to use the 5.x type information in the - // update script. - const downgradeCdkTask = context.addTask(new NodePackageInstallTask({ - packageName: '@angular/cdk@">=5 <6"' - })); - const downgradeMaterialTask = context.addTask(new NodePackageInstallTask({ - packageName: '@angular/material@">=5 <6"' + // Downgrade @angular/cdk and @angular/material to 5.x. This allows us to use the 5.x type + // information in the update script. + const downgradeTask = context.addTask(new NodePackageInstallTask({ + packageName: '@angular/cdk@">=5 <6" @angular/material@">=5 <6"' })); // Run the update tslint rules. @@ -63,16 +60,12 @@ export default function(): Rule { silent: false, ignoreErrors: true, tsConfigPath: './tsconfig.json', - }), [downgradeCdkTask, downgradeMaterialTask]); + }), [downgradeTask]); // Upgrade @angular/material back to 6.x. - const upgradeCdkTask = context.addTask(new NodePackageInstallTask({ - // TODO(mmalerba): Change "next" to ">=6 <7". - packageName: '@angular/cdk@next' - }), [updateTask]); - const upgradeMaterialTask = context.addTask(new NodePackageInstallTask({ - // TODO(mmalerba): Fix before submitting. - packageName: '/usr/local/google/home/mmalerba/material2/dist/releases/material/angular-material-6.0.0-rc.1.tgz' + const upgradeTask = context.addTask(new NodePackageInstallTask({ + // TODO(mmalerba): Change "next" to ">=6 <7". Change material back to npm package. + packageName: '@angular/cdk@next /usr/local/google/home/mmalerba/material2/dist/releases/material/angular-material-6.0.0-rc.1.tgz' }), [updateTask]); // Delete the temporary schematics directory. @@ -80,7 +73,7 @@ export default function(): Rule { new RunSchematicTask(path.join(__dirname, '../collection.json'), 'ng-post-update', { deleteFiles: updateSrcs .map(entry => entry.path.replace(schematicsSrcPath, schematicsTmpPath)) - }), [upgradeCdkTask, upgradeMaterialTask]); + }), [upgradeTask]); }; } From 30ce93d473f4bae98deb80f5d1ad91d9143a72fd Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 10 Apr 2018 15:04:24 -0700 Subject: [PATCH 11/11] update schematics version and fix TODOs --- package-lock.json | 31 ++++++++----------- package.json | 6 ++-- src/lib/schematics/collection.json | 13 ++++++-- .../update/material/typescript-specifiers.ts | 2 +- .../update/tslint/find-tslint-binary.ts | 2 +- src/lib/schematics/update/update.ts | 22 ++++++++----- 6 files changed, 42 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cd5e3b6b4eb..0816611da76b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@angular-devkit/core": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.4.9.tgz", - "integrity": "sha512-MOi8F6kiu9GKVzItO0NV0hPcjK/XfbeaMkBTewC/CWZKnNGeBmc3YJyBMRDaZXr5AxhzzxDDpQAso5oYJ2qXSw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.5.5.tgz", + "integrity": "sha512-91JZwom/wcZQQcm4oPnF3z6/rlKfIRdwB9eUe6htOL20K4M+Bsog7ngNYQhmOVX0QJfBQJX372gItd6vTaM9zg==", "dev": true, "requires": { "ajv": "5.5.2", @@ -25,11 +25,12 @@ } }, "@angular-devkit/schematics": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.4.9.tgz", - "integrity": "sha512-hRz2l0hX/sUMY7X5wJz6zGP4O8VOWwbdxBel/WbDGpiq/1IljfFZlh8L78k4tczL0cT+dkeMqzrh/TLvF48wNQ==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.5.5.tgz", + "integrity": "sha512-l3Pf69BBoZ7T3ju5DMeYVPC6ANidcJuJ9xA0+GRVoHSvNxl/roxJsQ56ujOXEChq/1znBgv6Mh8QBFycJIOQjA==", "dev": true, "requires": { + "@angular-devkit/core": "0.5.5", "@ngtools/json-schema": "1.1.0", "rxjs": "6.0.0-rc.0" } @@ -759,20 +760,14 @@ "dev": true }, "@schematics/angular": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.4.9.tgz", - "integrity": "sha512-rcujuPa4MSX2DqWH9ACAZZJpaCaUnAxWizSiIRtWM89/t8By+arECtTpYicOUBr8+XvXgoF9FB53yO1piY/jdw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.5.5.tgz", + "integrity": "sha512-PGrT5hNLzoM+jOavTZTz2Uca1IoRxtGpKl1E0axP2JFkm0/KIKRVTDzGLwTLsQNeBKeCIlgnoXax/B9Xnvg5Qw==", "dev": true, "requires": { - "typescript": "2.6.2" - }, - "dependencies": { - "typescript": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", - "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", - "dev": true - } + "@angular-devkit/core": "0.5.5", + "@angular-devkit/schematics": "0.5.5", + "typescript": "2.7.2" } }, "@sindresorhus/is": { diff --git a/package.json b/package.json index dee0541e7290..a3647a675f28 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "zone.js": "^0.8.4" }, "devDependencies": { - "@angular-devkit/core": "^0.5.4", - "@angular-devkit/schematics": "file:../devkit/dist/@angular-devkit/schematics/angular-devkit-schematics-0.5.4.tgz", + "@angular-devkit/core": "^0.5.5", + "@angular-devkit/schematics": "^0.5.5", "@angular/bazel": "6.0.0-rc.3", "@angular/compiler-cli": "6.0.0-rc.3", "@angular/http": "6.0.0-rc.3", @@ -51,7 +51,7 @@ "@angular/upgrade": "6.0.0-rc.3", "@bazel/ibazel": "0.3.1", "@google-cloud/storage": "^1.1.1", - "@schematics/angular": "^0.5.4", + "@schematics/angular": "^0.5.5", "@types/chalk": "^0.4.31", "@types/fs-extra": "^4.0.3", "@types/glob": "^5.0.33", diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json index 075971d4e10d..e9f3f61c7eea 100644 --- a/src/lib/schematics/collection.json +++ b/src/lib/schematics/collection.json @@ -9,16 +9,23 @@ "schema": "./shell/schema.json", "aliases": ["material-shell"] }, + + // Group of schematics used to update Angular CDK and Angular Material. "ng-update": { "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", "factory": "./update/update" }, "ng-post-update": { - "description": "Updates API usage for the most recent major version of Angular CDK and Angular Material", + "description": "Performs cleanup after ng-update.", "factory": "./update/update#postUpdate", - // TODO(mmalerba): put back. - // "private": true + "private": true + }, + "ng-post-post-update": { + "description": "Logs completion message for ng-update after ng-post-update.", + "factory": "./update/update#postPostUpdate", + "private": true }, + // Create a dashboard component "materialDashboard": { "description": "Create a card-based dashboard component", diff --git a/src/lib/schematics/update/material/typescript-specifiers.ts b/src/lib/schematics/update/material/typescript-specifiers.ts index b3c3c000efb4..e5db37f77cff 100644 --- a/src/lib/schematics/update/material/typescript-specifiers.ts +++ b/src/lib/schematics/update/material/typescript-specifiers.ts @@ -22,4 +22,4 @@ function isMaterialDeclaration(declaration: ts.ImportDeclaration | ts.ExportDecl const moduleSpecifier = declaration.moduleSpecifier.getText(); return moduleSpecifier.indexOf(materialModuleSpecifier) !== -1|| moduleSpecifier.indexOf(cdkModuleSpecifier) !== -1; -} \ No newline at end of file +} diff --git a/src/lib/schematics/update/tslint/find-tslint-binary.ts b/src/lib/schematics/update/tslint/find-tslint-binary.ts index 2326a2101053..37baf9de9231 100644 --- a/src/lib/schematics/update/tslint/find-tslint-binary.ts +++ b/src/lib/schematics/update/tslint/find-tslint-binary.ts @@ -13,4 +13,4 @@ export function findTslintBinaryPath() { } else { return resolveBinSync('tslint', 'tslint'); } -} \ No newline at end of file +} diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 2880888c4fcb..fe254659f72f 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -1,4 +1,4 @@ -import {FileEntry, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import {chain, FileEntry, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; import { NodePackageInstallTask, RunSchematicTask, @@ -64,13 +64,13 @@ export default function(): Rule { // Upgrade @angular/material back to 6.x. const upgradeTask = context.addTask(new NodePackageInstallTask({ - // TODO(mmalerba): Change "next" to ">=6 <7". Change material back to npm package. - packageName: '@angular/cdk@next /usr/local/google/home/mmalerba/material2/dist/releases/material/angular-material-6.0.0-rc.1.tgz' + // TODO(mmalerba): Change "next" to ">=6 <7". + packageName: '@angular/cdk@next @angular/material@next' }), [updateTask]); // Delete the temporary schematics directory. context.addTask( - new RunSchematicTask(path.join(__dirname, '../collection.json'), 'ng-post-update', { + new RunSchematicTask('ng-post-update', { deleteFiles: updateSrcs .map(entry => entry.path.replace(schematicsSrcPath, schematicsTmpPath)) }), [upgradeTask]); @@ -79,12 +79,18 @@ export default function(): Rule { /** Post-update schematic to be called when ng update is finished. */ export function postUpdate(options: {deleteFiles: string[]}): Rule { - return (tree: Tree) => { + return (tree: Tree, context: SchematicContext) => { for (let file of options.deleteFiles) { tree.delete(file); } - console.log('\nComplete! Please check the output above for any issues that were detected but' + - ' could not be automatically fixed.'); - } + context.addTask(new RunSchematicTask('ng-post-post-update', {})); + }; +} + +/** Post-post-update schematic to be called when post-update is finished. */ +export function postPostUpdate(): Rule { + return () => console.log( + '\nComplete! Please check the output above for any issues that were detected but could not' + + ' be automatically fixed.'); }