Skip to content

Commit ea56863

Browse files
committed
feat(material/ng-update): add migration for hammerjs in version 9
1 parent 473d4c6 commit ea56863

27 files changed

+2396
-74
lines changed

src/cdk/private/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
export * from './expect-async-error';
1010
export * from './wrapped-error-message';
1111
export * from './mock-ng-zone';
12+
export * from './text-dedent';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Template string function that can be used to dedent a given string
11+
* literal. The smallest common indentation will be omitted.
12+
*/
13+
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
14+
let joinedString = '';
15+
for (let i = 0; i < values.length; i++) {
16+
joinedString += `${strings[i]}${values[i]}`;
17+
}
18+
joinedString += strings[strings.length - 1];
19+
20+
const matches = joinedString.match(/^[ \t]*(?=\S)/gm);
21+
if (matches === null) {
22+
return joinedString;
23+
}
24+
25+
const minLineIndent = Math.min(...matches.map(el => el.length));
26+
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
27+
return minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
28+
}

src/cdk/schematics/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@
99
export * from './utils';
1010
export * from './ng-update/public-api';
1111
export * from './update-tool/public-api';
12+
13+
// Re-export parse5 from the CDK. Material schematics code cannot simply import
14+
// "parse5" because it could result in a different version. As long as we import
15+
// it from within the CDK, it will always be the correct version that is specified
16+
// in the CDK "package.json" as optional dependency.
17+
import * as parse5 from 'parse5';
18+
export {parse5};

src/cdk/schematics/ng-update/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function onMigrationComplete(targetVersion: TargetVersion, hasFailures: boolean)
4040

4141
if (hasFailures) {
4242
console.log(chalk.yellow(
43-
' ⚠ Some issues were detected but could not be fixed automatically. Please check the ' +
44-
'output above and fix these issues manually.'));
43+
' ⚠ Some issues were detected but could not be fixed automatically. Please check the ' +
44+
'output above and fix these issues manually.'));
4545
}
4646
}

src/cdk/schematics/ng-update/upgrade-rules/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
10+
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
1011

1112
import {Constructor, runMigrationRules} from '../../update-tool';
1213
import {MigrationRule} from '../../update-tool/migration-rule';
@@ -51,7 +52,7 @@ type NullableMigrationRule = Constructor<MigrationRule<RuleUpgradeData|null>>;
5152
export function createUpgradeRule(
5253
targetVersion: TargetVersion, extraRules: NullableMigrationRule[], upgradeData: RuleUpgradeData,
5354
onMigrationCompleteFn?: (targetVersion: TargetVersion, hasFailures: boolean) => void): Rule {
54-
return (tree: Tree, context: SchematicContext) => {
55+
return async (tree: Tree, context: SchematicContext) => {
5556
const logger = context.logger;
5657
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
5758

@@ -67,13 +68,30 @@ export function createUpgradeRule(
6768
// we don't want to check these again, as this would result in duplicated failure messages.
6869
const analyzedFiles = new Set<string>();
6970
let hasRuleFailures = false;
71+
let needsPackageManagerRun = false;
7072

71-
for (const tsconfigPath of [...buildPaths, ...testPaths]) {
72-
hasRuleFailures = hasRuleFailures || runMigrationRules(
73-
tree, context.logger, tsconfigPath, targetVersion, [...cdkMigrationRules, ...extraRules],
74-
upgradeData, analyzedFiles);
73+
const runMigration = (tsconfigPath: string, isTestTarget: boolean) => {
74+
const result = runMigrationRules(
75+
tree, context.logger, tsconfigPath, isTestTarget, targetVersion,
76+
[...cdkMigrationRules, ...extraRules], upgradeData, analyzedFiles);
77+
78+
hasRuleFailures = hasRuleFailures || result.hasFailures;
79+
needsPackageManagerRun = needsPackageManagerRun || result.needsPackageManagerRun;
80+
};
81+
82+
buildPaths.forEach(p => runMigration(p, false));
83+
testPaths.forEach(p => runMigration(p, true));
84+
85+
// Migrations can be require the package manager to run upon completion.
86+
// e.g. to update the lock file when a dependency has been removed.
87+
if (needsPackageManagerRun) {
88+
context.addTask(new NodePackageInstallTask({quiet: true}));
7589
}
7690

91+
// Execute all asynchronous tasks and await them here. We want to run
92+
// the "onMigrationCompleteFn" after all work is done.
93+
await context.engine.executePostTasks().toPromise();
94+
7795
if (onMigrationCompleteFn) {
7896
onMigrationCompleteFn(targetVersion, hasRuleFailures);
7997
}

src/cdk/schematics/testing/test-case-setup.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/test
1414
import {readFileSync, removeSync} from 'fs-extra';
1515
import {sync as globSync} from 'glob';
1616
import {basename, extname, join, relative, sep} from 'path';
17+
import {EMPTY} from 'rxjs';
1718
import {createTestApp} from '../testing';
1819

1920
/** Suffix that indicates whether a given file is a test case input. */
@@ -53,7 +54,15 @@ export async function createFileSystemTestApp(runner: SchematicTestRunner) {
5354
};
5455

5556
function writeFile(filePath: string, content: string) {
57+
// Update the temp file system host to reflect the changes in the real file system.
58+
// This is still necessary since we depend on the real file system for parsing the
59+
// TypeScript project.
5660
tempFileSystemHost.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(content));
61+
if (hostTree.exists(filePath)) {
62+
hostTree.overwrite(filePath, content);
63+
} else {
64+
hostTree.create(filePath, content);
65+
}
5766
}
5867
}
5968

@@ -95,6 +104,12 @@ export async function createTestCaseSetup(migrationName: string, collectionPath:
95104
// from within the project.
96105
process.chdir(tempPath);
97106

107+
// Patch "executePostTasks" to do nothing. This is necessary since
108+
// we cannot run the node install task in unit tests. Rather we just
109+
// assert that certain async post tasks are scheduled.
110+
// TODO(devversion): RxJS version conflicts between angular-devkit and our dev deps.
111+
runner.engine.executePostTasks = () => EMPTY as any;
112+
98113
await runner.runSchematicAsync(migrationName, {}, appTree).toPromise();
99114

100115
// Switch back to the initial working directory.
@@ -103,7 +118,7 @@ export async function createTestCaseSetup(migrationName: string, collectionPath:
103118
return {logOutput};
104119
};
105120

106-
return {appTree, writeFile, tempPath, removeTempDir, runFixers};
121+
return {runner, appTree, writeFile, tempPath, removeTempDir, runFixers};
107122
}
108123

109124
/**

src/cdk/schematics/update-tool/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import {parseTsconfigFile} from './utils/parse-tsconfig';
2020
export type Constructor<T> = new (...args: any[]) => T;
2121

2222
export function runMigrationRules<T>(
23-
tree: Tree, logger: logging.LoggerApi, tsconfigPath: string, targetVersion: TargetVersion,
24-
ruleTypes: Constructor<MigrationRule<T>>[], upgradeData: T,
25-
analyzedFiles: Set<string>): boolean {
23+
tree: Tree, logger: logging.LoggerApi, tsconfigPath: string, isTestTarget: boolean,
24+
targetVersion: TargetVersion, ruleTypes: Constructor<MigrationRule<T>>[], upgradeData: T,
25+
analyzedFiles: Set<string>): {hasFailures: boolean, needsPackageManagerRun: boolean} {
2626
// The CLI uses the working directory as the base directory for the
2727
// virtual file system tree.
2828
const basePath = process.cwd();
@@ -44,8 +44,9 @@ export function runMigrationRules<T>(
4444

4545
// Create instances of all specified migration rules.
4646
for (const ruleCtor of ruleTypes) {
47-
const rule = new ruleCtor(program, typeChecker, targetVersion, upgradeData);
48-
rule.getUpdateRecorder = getUpdateRecorder;
47+
const rule = new ruleCtor(
48+
program, typeChecker, targetVersion, upgradeData, tree, getUpdateRecorder, basePath, logger,
49+
isTestTarget);
4950
rule.init();
5051
if (rule.ruleEnabled) {
5152
rules.push(rule);
@@ -102,6 +103,9 @@ export function runMigrationRules<T>(
102103
}
103104
});
104105

106+
// Call the "postAnalysis" method for each migration rule.
107+
rules.forEach(r => r.postAnalysis());
108+
105109
// Commit all recorded updates in the update recorder. We need to perform the
106110
// replacements per source file in order to ensure that offsets in the TypeScript
107111
// program are not incorrectly shifted.
@@ -120,7 +124,10 @@ export function runMigrationRules<T>(
120124
});
121125
}
122126

123-
return !!ruleFailures.length;
127+
return {
128+
hasFailures: !!ruleFailures.length,
129+
needsPackageManagerRun: rules.some(r => r.needsPackageManagerRun),
130+
};
124131

125132
function getUpdateRecorder(filePath: string): UpdateRecorder {
126133
const treeFilePath = getProjectRelativePath(filePath);

src/cdk/schematics/update-tool/migration-rule.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {UpdateRecorder} from '@angular-devkit/schematics';
9+
import {logging} from '@angular-devkit/core';
10+
import {Tree, UpdateRecorder} from '@angular-devkit/schematics';
1011
import * as ts from 'typescript';
1112
import {ResolvedResource} from './component-resource-collector';
1213
import {TargetVersion} from './target-version';
@@ -25,13 +26,38 @@ export class MigrationRule<T> {
2526
/** Whether the migration rule is enabled or not. */
2627
ruleEnabled = true;
2728

29+
/** Whether the package manager should run upon migration completion. */
30+
needsPackageManagerRun = false;
31+
2832
constructor(
29-
public program: ts.Program, public typeChecker: ts.TypeChecker,
30-
public targetVersion: TargetVersion, public upgradeData: T) {}
33+
/** TypeScript program for the migration. */
34+
public program: ts.Program,
35+
/** TypeChecker instance for the analysis program. */
36+
public typeChecker: ts.TypeChecker,
37+
/** Version for which the migration rule should run. */
38+
public targetVersion: TargetVersion,
39+
/** Upgrade data passed to the migration. */
40+
public upgradeData: T,
41+
/** Devkit tree for the current migration. Can be used to insert/remove files. */
42+
public tree: Tree,
43+
/** Gets the update recorder for a given source file or resolved template. */
44+
public getUpdateRecorder: (filePath: string) => UpdateRecorder,
45+
/** Base directory of the virtual file system tree. */
46+
public basePath: string,
47+
/** Logger that can be used to print messages as part of the migration. */
48+
public logger: logging.LoggerApi,
49+
/** Whether the migration runs for a test target. */
50+
public isTestTarget: boolean) {}
3151

3252
/** Method can be used to perform global analysis of the program. */
3353
init(): void {}
3454

55+
/**
56+
* Method that will be called once all nodes, templates and stylesheets
57+
* have been visited.
58+
*/
59+
postAnalysis(): void {}
60+
3561
/**
3662
* Method that will be called for each node in a given source file. Unlike tslint, this
3763
* function will only retrieve TypeScript nodes that need to be casted manually. This
@@ -46,11 +72,6 @@ export class MigrationRule<T> {
4672
/** Method that will be called for each stylesheet in the program. */
4773
visitStylesheet(stylesheet: ResolvedResource): void {}
4874

49-
/** Gets the update recorder for a given source file or resolved template. */
50-
getUpdateRecorder(filePath: string): UpdateRecorder {
51-
throw new Error('MigrationRule#getUpdateRecorder is not implemented.');
52-
}
53-
5475
/** Creates a failure with a specified message at the given node location. */
5576
createFailureAtNode(node: ts.Node, message: string) {
5677
const sourceFile = node.getSourceFile();

src/cdk/schematics/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './build-component';
1212
export * from './get-project';
1313
export * from './html-head-element';
1414
export * from './parse5-element';
15+
export * from './project-index-file';
1516
export * from './project-main-file';
1617
export * from './project-style-file';
1718
export * from './project-targets';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {WorkspaceProject} from '@angular-devkit/core/src/experimental/workspace';
10+
import {BrowserBuilderTarget} from '@schematics/angular/utility/workspace-models';
11+
import {defaultTargetBuilders, getTargetsByBuilderName} from './project-targets';
12+
13+
/** Gets the path of the index file in the given project. */
14+
export function getProjectIndexFiles(project: WorkspaceProject): string[] {
15+
// Use a set to remove duplicate index files referenced in multiple build targets
16+
// of a project.
17+
return [...new Set(
18+
(getTargetsByBuilderName(project, defaultTargetBuilders.build) as BrowserBuilderTarget[])
19+
.filter(t => t.options.index)
20+
.map(t => t.options.index!))];
21+
}

src/cdk/schematics/utils/project-targets.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,37 @@
88

99
import {WorkspaceProject} from '@angular-devkit/core/src/experimental/workspace';
1010
import {SchematicsException} from '@angular-devkit/schematics';
11+
import {BuilderTarget} from '@schematics/angular/utility/workspace-models';
12+
13+
/** Object that maps a CLI target to its default builder name. */
14+
export const defaultTargetBuilders = {
15+
build: '@angular-devkit/build-angular:browser',
16+
test: '@angular-devkit/build-angular:karma',
17+
};
1118

1219
/** Resolves the architect options for the build target of the given project. */
1320
export function getProjectTargetOptions(project: WorkspaceProject, buildTarget: string) {
14-
if (project.targets &&
15-
project.targets[buildTarget] &&
16-
project.targets[buildTarget].options) {
17-
21+
if (project.targets && project.targets[buildTarget] && project.targets[buildTarget].options) {
1822
return project.targets[buildTarget].options;
1923
}
2024

2125
// TODO(devversion): consider removing this architect check if the CLI completely switched
2226
// over to `targets`, and the `architect` support has been removed.
2327
// See: https://github.com/angular/angular-cli/commit/307160806cb48c95ecb8982854f452303801ac9f
24-
if (project.architect &&
25-
project.architect[buildTarget] &&
28+
if (project.architect && project.architect[buildTarget] &&
2629
project.architect[buildTarget].options) {
27-
2830
return project.architect[buildTarget].options;
2931
}
3032

3133
throw new SchematicsException(
32-
`Cannot determine project target configuration for: ${buildTarget}.`);
34+
`Cannot determine project target configuration for: ${buildTarget}.`);
35+
}
36+
37+
/** Gets all targets from the given project that match the specified builder name. */
38+
export function getTargetsByBuilderName(
39+
project: WorkspaceProject, builderName: string): BuilderTarget<any, unknown>[] {
40+
const targets = project.targets || project.architect || {};
41+
return Object.keys(targets)
42+
.filter(name => targets[name].builder === builderName)
43+
.map(name => targets[name]);
3344
}

src/material/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ filegroup(
88
name = "schematics_assets",
99
srcs = glob([
1010
"ng-generate/*/files/**/*",
11+
"ng-update/upgrade-rules/hammer-gestures-v9/gesture-config.template",
1112
"**/*.json",
1213
]),
1314
)
@@ -70,6 +71,7 @@ ts_library(
7071
tsconfig = ":tsconfig.json",
7172
deps = [
7273
":schematics",
74+
"//src/cdk/private/testing",
7375
"//src/cdk/schematics",
7476
"//src/cdk/schematics/testing",
7577
"@npm//@angular-devkit/core",

src/material/schematics/ng-add/fonts/material-fonts.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,35 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Tree} from '@angular-devkit/schematics';
10-
import {appendHtmlElementToHead, getProjectFromWorkspace} from '@angular/cdk/schematics';
9+
import {SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {
11+
appendHtmlElementToHead,
12+
getProjectFromWorkspace,
13+
getProjectIndexFiles,
14+
} from '@angular/cdk/schematics';
1115
import {getWorkspace} from '@schematics/angular/utility/config';
1216
import {Schema} from '../schema';
13-
import {getIndexHtmlPath} from './project-index-html';
1417

1518
/** Adds the Material Design fonts to the index HTML file. */
1619
export function addFontsToIndex(options: Schema): (host: Tree) => Tree {
1720
return (host: Tree) => {
1821
const workspace = getWorkspace(host);
1922
const project = getProjectFromWorkspace(workspace, options.project);
20-
const projectIndexHtmlPath = getIndexHtmlPath(project);
23+
const projectIndexFiles = getProjectIndexFiles(project);
24+
25+
if (!projectIndexFiles.length) {
26+
throw new SchematicsException('No project index HTML file could be found.');
27+
}
2128

2229
const fonts = [
2330
'https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap',
2431
'https://fonts.googleapis.com/icon?family=Material+Icons',
2532
];
2633

2734
fonts.forEach(f => {
28-
appendHtmlElementToHead(host, projectIndexHtmlPath, `<link href="${f}" rel="stylesheet">`);
35+
projectIndexFiles.forEach(indexFilePath => {
36+
appendHtmlElementToHead(host, indexFilePath, `<link href="${f}" rel="stylesheet">`);
37+
});
2938
});
3039

3140
return host;

0 commit comments

Comments
 (0)