Skip to content

Commit 668f791

Browse files
authored
refactor: decouple migrations from CLI devkit (#19014)
* 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. * Address feedback
1 parent 4136a70 commit 668f791

File tree

72 files changed

+876
-549
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

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

0 commit comments

Comments
 (0)