Skip to content

Commit bb54ff1

Browse files
committed
refactor: decouple migrations from CLI devkit (#19004)
We want to refactor our update-tool to no longer rely on specifics from the Angular CLI devkit. This has various benefits: * Ability to run migrations inside Google. * Ability to run migrations in a standalone tool (for people not using the CLI!) * No tight dependency on the CLI devkit API. We could easily switch tools if needed. * Ability to wrap migrations in tslint rules (or other tools). * And potentially more. These are just the ones that came to mind yet.
1 parent 6a64130 commit bb54ff1

File tree

72 files changed

+816
-544
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+816
-544
lines changed

src/cdk/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ ts_library(
3232
"@npm//@angular-devkit/schematics",
3333
# TODO(devversion): Only include jasmine for test sources (See: tsconfig types).
3434
"@npm//@types/jasmine",
35+
"@npm//@types/glob",
3536
"@npm//@types/node",
3637
"@npm//glob",
3738
"@npm//parse5",
@@ -73,6 +74,7 @@ ts_library(
7374
":schematics",
7475
"//src/cdk/schematics/testing",
7576
"//src/cdk/schematics/update-tool",
77+
"//src/cdk/testing/private",
7678
"@npm//@angular-devkit/schematics",
7779
"@npm//@schematics/angular",
7880
"@npm//@types/jasmine",

src/cdk/schematics/index.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Path to the schematic collection for non-migration schematics. Needs to use
3+
* the workspace path as otherwise the resolution won't work on Windows.
4+
*/
5+
export const COLLECTION_PATH = require
6+
.resolve('angular_material/src/cdk/schematics/collection.json');
7+
8+
/**
9+
* Path to the schematic collection that includes the migrations. Needs to use
10+
* the workspace path as otherwise the resolution won't work on Windows.
11+
*/
12+
export const MIGRATION_PATH = require
13+
.resolve('angular_material/src/cdk/schematics/migration.json');

src/cdk/schematics/ng-add/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Tree} from '@angular-devkit/schematics';
22
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
3+
import {COLLECTION_PATH} from '../index.spec';
34
import {createTestApp, getFileContent} from '../testing';
45
import {addPackageToPackageJson} from './package-config';
56

@@ -8,7 +9,7 @@ describe('CDK ng-add', () => {
89
let appTree: Tree;
910

1011
beforeEach(async () => {
11-
runner = new SchematicTestRunner('schematics', require.resolve('../collection.json'));
12+
runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
1213
appTree = await createTestApp(runner);
1314
});
1415

src/cdk/schematics/ng-generate/drag-drop/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
22
import {getProjectFromWorkspace} from '@angular/cdk/schematics';
33
import {getWorkspace} from '@schematics/angular/utility/config';
4+
import {COLLECTION_PATH} from '../../index.spec';
45
import {createTestApp, getFileContent} from '../../testing';
56
import {Schema} from './schema';
67

@@ -16,7 +17,7 @@ describe('CDK drag-drop schematic', () => {
1617
};
1718

1819
beforeEach(() => {
19-
runner = new SchematicTestRunner('schematics', require.resolve('../../collection.json'));
20+
runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
2021
});
2122

2223
it('should create drag-drop files and add them to module', async () => {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 {normalize} from '@angular-devkit/core';
10+
import {Tree, UpdateRecorder} from '@angular-devkit/schematics';
11+
import {relative} from 'path';
12+
import {FileSystem} from '../update-tool/file-system';
13+
14+
/** File system that leverages the virtual tree from the CLI devkit. */
15+
export class DevkitFileSystem implements FileSystem {
16+
private _updateRecorderCache = new Map<string, UpdateRecorder>();
17+
18+
constructor(private _tree: Tree, private _workspaceFsPath: string) {}
19+
20+
resolve(fsFilePath: string) {
21+
return normalize(relative(this._workspaceFsPath, fsFilePath)) as string;
22+
}
23+
24+
edit(fsFilePath: string) {
25+
const treeFilePath = this.resolve(fsFilePath);
26+
if (this._updateRecorderCache.has(treeFilePath)) {
27+
return this._updateRecorderCache.get(treeFilePath)!;
28+
}
29+
const recorder = this._tree.beginUpdate(treeFilePath);
30+
this._updateRecorderCache.set(treeFilePath, recorder);
31+
return recorder;
32+
}
33+
34+
commitEdits() {
35+
this._updateRecorderCache.forEach(r => this._tree.commitUpdate(r));
36+
this._updateRecorderCache.clear();
37+
}
38+
39+
exists(fsFilePath: string) {
40+
return this._tree.exists(this.resolve(fsFilePath));
41+
}
42+
43+
overwrite(fsFilePath: string, content: string) {
44+
this._tree.overwrite(this.resolve(fsFilePath), content);
45+
}
46+
47+
create(fsFilePath: string, content: string) {
48+
this._tree.create(this.resolve(fsFilePath), content);
49+
}
50+
51+
delete(fsFilePath: string) {
52+
this._tree.delete(this.resolve(fsFilePath));
53+
}
54+
55+
read(fsFilePath: string) {
56+
const buffer = this._tree.read(this.resolve(fsFilePath));
57+
return buffer !== null ? buffer.toString() : null;
58+
}
59+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
10+
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
11+
import {WorkspaceProject} from '@schematics/angular/utility/workspace-models';
12+
import {sync as globSync} from 'glob';
13+
import {join} from 'path';
14+
15+
import {UpdateProject} from '../update-tool';
16+
import {TargetVersion} from '../update-tool/target-version';
17+
import {getTargetTsconfigPath, getWorkspaceConfigGracefully} from '../utils/project-tsconfig-paths';
18+
19+
import {DevkitFileSystem} from './devkit-file-system';
20+
import {DevkitContext, DevkitMigrationCtor} from './devkit-migration';
21+
import {AttributeSelectorsMigration} from './migrations/attribute-selectors';
22+
import {ClassInheritanceMigration} from './migrations/class-inheritance';
23+
import {ClassNamesMigration} from './migrations/class-names';
24+
import {ConstructorSignatureMigration} from './migrations/constructor-signature';
25+
import {CssSelectorsMigration} from './migrations/css-selectors';
26+
import {ElementSelectorsMigration} from './migrations/element-selectors';
27+
import {InputNamesMigration} from './migrations/input-names';
28+
import {MethodCallArgumentsMigration} from './migrations/method-call-arguments';
29+
import {MiscTemplateMigration} from './migrations/misc-template';
30+
import {OutputNamesMigration} from './migrations/output-names';
31+
import {PropertyNamesMigration} from './migrations/property-names';
32+
import {UpgradeData} from './upgrade-data';
33+
34+
35+
/** List of migrations which run for the CDK update. */
36+
export const cdkMigrations: DevkitMigrationCtor<UpgradeData>[] = [
37+
AttributeSelectorsMigration,
38+
ClassInheritanceMigration,
39+
ClassNamesMigration,
40+
ConstructorSignatureMigration,
41+
CssSelectorsMigration,
42+
ElementSelectorsMigration,
43+
InputNamesMigration,
44+
MethodCallArgumentsMigration,
45+
MiscTemplateMigration,
46+
OutputNamesMigration,
47+
PropertyNamesMigration,
48+
];
49+
50+
type NullableMigration = DevkitMigrationCtor<UpgradeData|null>;
51+
52+
type PostMigrationFn =
53+
(context: SchematicContext, targetVersion: TargetVersion, hasFailure: boolean) => void;
54+
55+
/**
56+
* Creates a Angular schematic rule that runs the upgrade for the
57+
* specified target version.
58+
*/
59+
export function createMigrationSchematicRule(
60+
targetVersion: TargetVersion, extraMigrations: NullableMigration[], upgradeData: UpgradeData,
61+
onMigrationCompleteFn?: PostMigrationFn): Rule {
62+
return async (tree: Tree, context: SchematicContext) => {
63+
const logger = context.logger;
64+
const workspace = getWorkspaceConfigGracefully(tree);
65+
66+
if (workspace === null) {
67+
logger.error('Could not find workspace configuration file.');
68+
return;
69+
}
70+
71+
// Keep track of all project source files which have been checked/migrated. This is
72+
// necessary because multiple TypeScript projects can contain the same source file and
73+
// we don't want to check these again, as this would result in duplicated failure messages.
74+
const analyzedFiles = new Set<string>();
75+
// The CLI uses the working directory as the base directory for the virtual file system tree.
76+
const workspaceFsPath = process.cwd();
77+
const fileSystem = new DevkitFileSystem(tree, workspaceFsPath);
78+
const projectNames = Object.keys(workspace.projects);
79+
const migrations: NullableMigration[] = [...cdkMigrations, ...extraMigrations];
80+
let hasFailures = false;
81+
82+
for (const projectName of projectNames) {
83+
const project = workspace.projects[projectName];
84+
const buildTsconfigPath = getTargetTsconfigPath(project, 'build');
85+
const testTsconfigPath = getTargetTsconfigPath(project, 'test');
86+
87+
if (!buildTsconfigPath && !testTsconfigPath) {
88+
logger.warn(`Could not find TypeScript project for project: ${projectName}`);
89+
continue;
90+
}
91+
if (buildTsconfigPath !== null) {
92+
runMigrations(project, buildTsconfigPath, false);
93+
}
94+
if (testTsconfigPath !== null) {
95+
runMigrations(project, testTsconfigPath, true);
96+
}
97+
}
98+
99+
let runPackageManager = false;
100+
// Run the global post migration static members for all migrations.
101+
migrations.forEach(m => {
102+
const actionResult =
103+
m.globalPostMigration !== undefined ? m.globalPostMigration(tree, context) : null;
104+
if (actionResult) {
105+
runPackageManager = runPackageManager || actionResult.runPackageManager;
106+
}
107+
});
108+
109+
// If a migration requested the package manager to run, we run it as an
110+
// asynchronous post migration task. We cannot run it synchronously,
111+
// as file changes from the current migration task are not applied to
112+
// the file system yet.
113+
if (runPackageManager) {
114+
context.addTask(new NodePackageInstallTask({quiet: false}));
115+
}
116+
117+
if (onMigrationCompleteFn) {
118+
onMigrationCompleteFn(context, targetVersion, hasFailures);
119+
}
120+
121+
/** Runs the migrations for the specified workspace project. */
122+
function runMigrations(project: WorkspaceProject, tsconfigPath: string, isTestTarget: boolean) {
123+
const projectRootFsPath = join(workspaceFsPath, project.root);
124+
const tsconfigFsPath = join(workspaceFsPath, tsconfigPath);
125+
const program = UpdateProject.createProgramFromTsconfig(tsconfigFsPath, fileSystem);
126+
const updateContext: DevkitContext = {
127+
workspaceFsPath,
128+
isTestTarget,
129+
project,
130+
tree,
131+
};
132+
133+
const updateProject = new UpdateProject(
134+
updateContext,
135+
program,
136+
fileSystem,
137+
analyzedFiles,
138+
context.logger,
139+
);
140+
141+
// In some applications, developers will have global stylesheets which are not
142+
// specified in any Angular component. Therefore we glob up all CSS and SCSS files
143+
// outside of node_modules and dist. The files will be read by the individual
144+
// stylesheet rules and checked.
145+
// TODO: rework this to collect global stylesheets from the workspace config. COMP-280.
146+
const additionalStylesheets = globSync(
147+
'!(node_modules|dist)/**/*.+(css|scss)',
148+
{absolute: true, cwd: projectRootFsPath, nodir: true});
149+
150+
const result =
151+
updateProject.migrate(migrations, targetVersion, upgradeData, additionalStylesheets);
152+
153+
// Commit all recorded edits in the update recorder. We apply the edits after all
154+
// migrations ran because otherwise offsets in the TypeScript program would be
155+
// shifted and individual migrations could no longer update the same source file.
156+
fileSystem.commitEdits();
157+
158+
hasFailures = hasFailures || result.hasFailures;
159+
}
160+
};
161+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 {SchematicContext, Tree} from '@angular-devkit/schematics';
10+
import {WorkspaceProject} from '@schematics/angular/utility/workspace-models';
11+
import {Constructor, Migration, PostMigrationAction} from '../update-tool/migration';
12+
13+
export type DevkitContext = {
14+
/** Devkit tree for the current migrations. Can be used to insert/remove files. */
15+
tree: Tree,
16+
/** Workspace project the migrations run against. */
17+
project: WorkspaceProject,
18+
/** Absolute file system path to the workspace */
19+
workspaceFsPath: string,
20+
/** Whether the migrations run for a test target. */
21+
isTestTarget: boolean,
22+
};
23+
24+
export abstract class DevkitMigration<Data> extends Migration<Data, DevkitContext> {
25+
/**
26+
* Optional static method that will be called once the migration of all project
27+
* targets has been performed. This method can be used to make changes respecting the
28+
* migration result of all individual targets. e.g. removing HammerJS if it
29+
* is not needed in any project target.
30+
*/
31+
static globalPostMigration?(tree: Tree, context: SchematicContext): PostMigrationAction;
32+
}
33+
34+
export type DevkitMigrationCtor<Data> = Constructor<DevkitMigration<Data>> &
35+
{[m in keyof typeof DevkitMigration]: (typeof DevkitMigration)[m]};

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@
99
import {Rule, SchematicContext} from '@angular-devkit/schematics';
1010
import {TargetVersion} from '../update-tool/target-version';
1111
import {cdkUpgradeData} from './upgrade-data';
12-
import {createUpgradeRule} from './upgrade-rules';
12+
import {createMigrationSchematicRule} from './devkit-migration-rule';
1313

1414
/** Entry point for the migration schematics with target of Angular CDK 6.0.0 */
1515
export function updateToV6(): Rule {
16-
return createUpgradeRule(TargetVersion.V6, [], cdkUpgradeData, onMigrationComplete);
16+
return createMigrationSchematicRule(TargetVersion.V6, [], cdkUpgradeData, onMigrationComplete);
1717
}
1818

1919
/** Entry point for the migration schematics with target of Angular CDK 7.0.0 */
2020
export function updateToV7(): Rule {
21-
return createUpgradeRule(TargetVersion.V7, [], cdkUpgradeData, onMigrationComplete);
21+
return createMigrationSchematicRule(TargetVersion.V7, [], cdkUpgradeData, onMigrationComplete);
2222
}
2323

2424
/** Entry point for the migration schematics with target of Angular CDK 8.0.0 */
2525
export function updateToV8(): Rule {
26-
return createUpgradeRule(TargetVersion.V8, [], cdkUpgradeData, onMigrationComplete);
26+
return createMigrationSchematicRule(TargetVersion.V8, [], cdkUpgradeData, onMigrationComplete);
2727
}
2828

2929
/** Entry point for the migration schematics with target of Angular CDK 9.0.0 */
3030
export function updateToV9(): Rule {
31-
return createUpgradeRule(TargetVersion.V9, [], cdkUpgradeData, onMigrationComplete);
31+
return createMigrationSchematicRule(TargetVersion.V9, [], cdkUpgradeData, onMigrationComplete);
3232
}
3333

3434
/** Function that will be called when the migration completed. */

src/cdk/schematics/ng-update/upgrade-rules/attribute-selectors-rule.ts renamed to src/cdk/schematics/ng-update/migrations/attribute-selectors.ts

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

99
import * as ts from 'typescript';
1010
import {ResolvedResource} from '../../update-tool/component-resource-collector';
11-
import {MigrationRule} from '../../update-tool/migration-rule';
11+
import {Migration} from '../../update-tool/migration';
1212
import {AttributeSelectorUpgradeData} from '../data/attribute-selectors';
1313
import {findAllSubstringIndices} from '../typescript/literal';
14-
import {getVersionUpgradeData, RuleUpgradeData} from '../upgrade-data';
14+
import {getVersionUpgradeData, UpgradeData} from '../upgrade-data';
1515

1616
/**
17-
* Migration rule that walks through every string literal, template and stylesheet
17+
* Migration that walks through every string literal, template and stylesheet
1818
* in order to switch deprecated attribute selectors to the updated selector.
1919
*/
20-
export class AttributeSelectorsRule extends MigrationRule<RuleUpgradeData> {
20+
export class AttributeSelectorsMigration extends Migration<UpgradeData> {
2121
/** Required upgrade changes for specified target version. */
2222
data = getVersionUpgradeData(this, 'attributeSelectors');
2323

2424
// Only enable the migration rule if there is upgrade data.
25-
ruleEnabled = this.data.length !== 0;
25+
enabled = this.data.length !== 0;
2626

2727
visitNode(node: ts.Node) {
2828
if (ts.isStringLiteralLike(node)) {
@@ -68,8 +68,8 @@ export class AttributeSelectorsRule extends MigrationRule<RuleUpgradeData> {
6868
}
6969

7070
private _replaceSelector(filePath: string, start: number, data: AttributeSelectorUpgradeData) {
71-
const updateRecorder = this.getUpdateRecorder(filePath);
72-
updateRecorder.remove(start, data.replace.length);
73-
updateRecorder.insertRight(start, data.replaceWith);
71+
this.fileSystem.edit(filePath)
72+
.remove(start, data.replace.length)
73+
.insertRight(start, data.replaceWith);
7474
}
7575
}

0 commit comments

Comments
 (0)