Skip to content

Commit e197578

Browse files
fix(migrations): allow UpdateChanges to install packages before running migrations #8254 (#8310)
1 parent ee2fad4 commit e197578

File tree

5 files changed

+133
-101
lines changed

5 files changed

+133
-101
lines changed

projects/igniteui-angular/migrations/common/UpdateChanges.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import { SchematicContext, Tree, FileVisitor } from '@angular-devkit/schematics';
2-
import { WorkspaceSchema } from '@schematics/angular/utility/workspace-models';
3-
41
import * as fs from 'fs';
52
import * as path from 'path';
3+
import * as ts from 'typescript';
64
import * as tss from 'typescript/lib/tsserverlibrary';
5+
import { SchematicContext, Tree, FileVisitor } from '@angular-devkit/schematics';
6+
import { WorkspaceSchema } from '@schematics/angular/utility/workspace-models';
77
import {
88
ClassChanges, BindingChanges, SelectorChange,
99
SelectorChanges, ThemePropertyChanges, ImportsChanges, MemberChanges
1010
} from './schema';
1111
import {
1212
getLanguageService, getRenamePositions, findMatches,
13-
replaceMatch, createProjectService, isMemberIgniteUI
13+
replaceMatch, createProjectService, isMemberIgniteUI, NG_LANG_SERVICE_PACKAGE_NAME
1414
} from './tsUtils';
15-
import { getProjectPaths, getWorkspace, getProjects, escapeRegExp } from './util';
15+
import {
16+
getProjectPaths, getWorkspace, getProjects, escapeRegExp,
17+
getPackageManager, canResolvePackage, tryInstallPackage, tryUninstallPackage
18+
} from './util';
1619
import { ServerHost } from './ServerHost';
1720

1821
export enum InputPropertyType {
@@ -82,14 +85,23 @@ export class UpdateChanges {
8285
return this._sassFiles;
8386
}
8487

85-
private _service: tss.LanguageService;
86-
public get service(): tss.LanguageService {
88+
private _service: ts.LanguageService;
89+
public get service(): ts.LanguageService {
8790
if (!this._service) {
8891
this._service = getLanguageService(this.tsFiles, this.host);
8992
}
9093
return this._service;
9194
}
9295

96+
private _packageManager: 'npm' | 'yarn';
97+
private get packageManager(): 'npm' | 'yarn' {
98+
if (!this._packageManager) {
99+
this._packageManager = getPackageManager(this.host);
100+
}
101+
102+
return this._packageManager;
103+
}
104+
93105
/**
94106
* Create a new base schematic to apply changes
95107
* @param rootPath Root folder for the schematic to read configs, pass __dirname
@@ -111,16 +123,27 @@ export class UpdateChanges {
111123

112124
/** Apply configured changes to the Host Tree */
113125
public applyChanges() {
126+
const shouldInstallPkg = this.membersChanges && this.membersChanges.changes.length
127+
&& !canResolvePackage(NG_LANG_SERVICE_PACKAGE_NAME);
128+
if (shouldInstallPkg) {
129+
this.context.logger.info(`Installing temporary migration dependencies via ${this.packageManager}.`);
130+
tryInstallPackage(this.context, this.packageManager, NG_LANG_SERVICE_PACKAGE_NAME);
131+
}
132+
114133
this.updateTemplateFiles();
115134
this.updateTsFiles();
116135
this.updateMembers();
117-
118136
/** Sass files */
119137
if (this.themePropsChanges && this.themePropsChanges.changes.length) {
120138
for (const entryPath of this.sassFiles) {
121139
this.updateThemeProps(entryPath);
122140
}
123141
}
142+
143+
if (shouldInstallPkg) {
144+
this.context.logger.info(`Cleaning up temporary migration dependencies.`);
145+
tryUninstallPackage(this.context, this.packageManager, NG_LANG_SERVICE_PACKAGE_NAME);
146+
}
124147
}
125148

126149
/** Add condition function. */
Lines changed: 26 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,17 @@
1-
import * as fs from 'fs';
2-
import * as path from 'path';
31
import * as ts from 'typescript/lib/tsserverlibrary';
42

5-
function noop() { }
6-
7-
function nowString() {
8-
// E.g. "12:34:56.789"
9-
const d = new Date();
10-
return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`;
11-
}
12-
133
export class Logger implements ts.server.Logger {
14-
private fd = -1;
15-
private seq = 0;
16-
private inGroup = false;
17-
private firstInGroup = true;
18-
194
constructor(
205
private readonly traceToConsole: boolean,
21-
private readonly level: ts.server.LogLevel,
22-
private readonly logFilename?: string,
23-
) {
24-
if (logFilename) {
25-
try {
26-
const dir = path.dirname(logFilename);
27-
if (!fs.existsSync(dir)) {
28-
fs.mkdirSync(dir);
29-
}
30-
this.fd = fs.openSync(logFilename, 'w');
31-
} catch {
32-
// swallow the error and keep logging disabled if file cannot be opened
33-
}
34-
}
35-
}
36-
37-
static padStringRight(str: string, padding: string) {
38-
return (str + padding).slice(0, padding.length);
39-
}
6+
private readonly level: ts.server.LogLevel
7+
) { }
408

41-
public close() {
42-
if (this.fd >= 0) {
43-
fs.close(this.fd, noop);
44-
}
9+
public hasLevel(level: ts.server.LogLevel) {
10+
return this.loggingEnabled() && this.level >= level;
4511
}
4612

47-
public getLogFileName() {
48-
return this.logFilename;
13+
public loggingEnabled() {
14+
return this.traceToConsole;
4915
}
5016

5117
public perftrc(s: string) {
@@ -56,52 +22,31 @@ export class Logger implements ts.server.Logger {
5622
this.msg(s, ts.server.Msg.Info);
5723
}
5824

59-
public err(s: string) {
60-
this.msg(s, ts.server.Msg.Err);
61-
}
62-
63-
public startGroup() {
64-
this.inGroup = true;
65-
this.firstInGroup = true;
66-
}
67-
68-
public endGroup() {
69-
this.inGroup = false;
70-
}
71-
72-
public loggingEnabled() {
73-
return !!this.logFilename || this.traceToConsole;
74-
}
75-
76-
public hasLevel(level: ts.server.LogLevel) {
77-
return this.loggingEnabled() && this.level >= level;
78-
}
79-
8025
public msg(s: string, type: ts.server.Msg = ts.server.Msg.Err) {
81-
if (!this.canWrite) { return; }
82-
83-
s = `[${nowString()}] ${s}\n`;
84-
if (!this.inGroup || this.firstInGroup) {
85-
const prefix = Logger.padStringRight(type + ' ' + this.seq.toString(), ' ');
86-
s = prefix + s;
26+
if (type === ts.server.Msg.Info) {
27+
// TODO: Info ignored for now, hook into context in the future
28+
// console.log(s);
8729
}
88-
this.write(s);
89-
if (!this.inGroup) {
90-
this.seq++;
30+
if (type === ts.server.Msg.Err) {
31+
console.error(s);
32+
}
33+
if (type === ts.server.Msg.Perf) {
34+
console.warn(s);
9135
}
9236
}
9337

94-
private get canWrite() {
95-
return this.fd >= 0 || this.traceToConsole;
96-
}
38+
//#region Not implemented
39+
/* These methods are used to log to a file,
40+
we will only use the logger to log on the console.
41+
*/
42+
public close() { }
9743

98-
private write(s: string) {
99-
if (this.fd >= 0) {
100-
const buf = Buffer.from(s);
101-
fs.writeSync(this.fd, buf, 0, buf.length, /*position*/ null);
102-
}
103-
if (this.traceToConsole) {
104-
console.warn(s);
105-
}
44+
public startGroup() { }
45+
46+
public endGroup() { }
47+
48+
public getLogFileName() {
49+
return null;
10650
}
51+
//#endregion
10752
}

projects/igniteui-angular/migrations/common/tsUtils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { MemberChange } from './schema';
66
import { escapeRegExp } from './util';
77
import { Logger } from './tsLogger';
88

9-
export const PACKAGE_NAME = 'igniteui-angular';
9+
export const IG_PACKAGE_NAME = 'igniteui-angular';
10+
export const NG_LANG_SERVICE_PACKAGE_NAME = '@angular/language-service';
1011

1112
/** Returns an source file */
1213
// export function getFileSource(sourceText: string): ts.SourceFile {
@@ -68,7 +69,7 @@ export function getImportModulePositions(sourceText: string, startsWith: string)
6869
/** Filters out statements to named imports (e.g. `import {x, y}`) from PACKAGE_IMPORT */
6970
const namedImportFilter = (statement: ts.Statement) => {
7071
if (statement.kind === ts.SyntaxKind.ImportDeclaration &&
71-
((statement as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text === PACKAGE_NAME) {
72+
((statement as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text === IG_PACKAGE_NAME) {
7273

7374
const clause = (statement as ts.ImportDeclaration).importClause;
7475
return clause && clause.namedBindings && clause.namedBindings.kind === ts.SyntaxKind.NamedImports;
@@ -189,7 +190,7 @@ export function createProjectService(serverHost: tss.server.ServerHost): tss.ser
189190
useSingleInferredProject: true,
190191
useInferredProjectPerProjectRoot: true,
191192
/* will load only global plug-ins */
192-
globalPlugins: ['@angular/language-service'],
193+
globalPlugins: [NG_LANG_SERVICE_PACKAGE_NAME],
193194
allowLocalPluginLoads: false,
194195
typingsInstaller: tss.server.nullTypingsInstaller
195196
});
@@ -204,7 +205,7 @@ export function createProjectService(serverHost: tss.server.ServerHost): tss.ser
204205
]
205206
});
206207
projectService.configurePlugin({
207-
pluginName: '@angular/language-service',
208+
pluginName: NG_LANG_SERVICE_PACKAGE_NAME,
208209
configuration: {
209210
angularOnly: false,
210211
},
@@ -233,7 +234,7 @@ export function getTypeDefinitionAtPosition(langServ: tss.LanguageService, entry
233234
.getProgram()
234235
.getSourceFile(definition.fileName)
235236
.statements
236-
.filter(<(a: ts.Statement) => a is ts.ClassDeclaration>(m => m.kind === ts.SyntaxKind.ClassDeclaration))
237+
.filter(<(a: tss.Statement) => a is tss.ClassDeclaration>(m => m.kind === tss.SyntaxKind.ClassDeclaration))
237238
.find(m => m.name.getText() === definition.containerName);
238239
const member: ts.ClassElement = classDeclaration
239240
?.members
@@ -248,10 +249,10 @@ export function getTypeDefinitionAtPosition(langServ: tss.LanguageService, entry
248249
return null;
249250
}
250251

251-
export function isMemberIgniteUI(change: MemberChange, langServ: tss.LanguageService, entryPath: string, matchPosition: number) {
252+
export function isMemberIgniteUI(change: MemberChange, langServ: tss.LanguageService, entryPath: string, matchPosition: number): boolean {
252253
const typeDef = getTypeDefinitionAtPosition(langServ, entryPath, matchPosition - 1);
253254
if (!typeDef) { return false; }
254-
return typeDef.fileName.includes(PACKAGE_NAME)
255+
return typeDef.fileName.includes(IG_PACKAGE_NAME)
255256
&& change.definedIn.indexOf(typeDef.name) !== -1;
256257
}
257258

projects/igniteui-angular/migrations/common/util.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { normalize } from '@angular-devkit/core';
22
import * as path from 'path';
3-
import { Tree } from '@angular-devkit/schematics';
3+
import { SchematicContext, Tree } from '@angular-devkit/schematics';
44
import { WorkspaceSchema, WorkspaceProject, ProjectType } from '@schematics/angular/utility/workspace-models';
5+
import { execSync } from 'child_process';
56

67
const configPaths = ['/.angular.json', '/angular.json'];
78

@@ -16,7 +17,7 @@ export function getProjectPaths(config: WorkspaceSchema, appendPrefix = true): s
1617
const projects = getProjects(config);
1718
for (const proj of projects) {
1819
let sourcePath = path.join('/', proj.sourceRoot);
19-
if ( appendPrefix && (proj.prefix || globalPrefix) ) {
20+
if (appendPrefix && (proj.prefix || globalPrefix)) {
2021
sourcePath = path.join(sourcePath, proj.prefix || globalPrefix);
2122
}
2223
sourceDirs.push(normalize(sourcePath));
@@ -53,3 +54,65 @@ export function getProjects(config: WorkspaceSchema): WorkspaceProject<ProjectTy
5354
export function escapeRegExp(string) {
5455
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
5556
}
57+
58+
export function supports(name: string): boolean {
59+
try {
60+
execSync(`${name} --version`, { stdio: 'ignore' });
61+
return true;
62+
} catch {
63+
return false;
64+
}
65+
}
66+
67+
export function getPackageManager(host: Tree) {
68+
const hasYarn = supports('yarn');
69+
const hasYarnLock = host.exists('yarn.lock');
70+
if (hasYarn && hasYarnLock) {
71+
return 'yarn';
72+
}
73+
return 'npm';
74+
}
75+
76+
export function canResolvePackage(pkg: string): boolean {
77+
let modulePath;
78+
try {
79+
modulePath = require.resolve(pkg);
80+
} finally {
81+
return !!modulePath;
82+
}
83+
}
84+
85+
export function tryInstallPackage(context: SchematicContext, packageManager: string, pkg: string) {
86+
try {
87+
context.logger.debug(`Installing ${pkg} via ${packageManager}.`);
88+
switch (packageManager) {
89+
case 'yarn':
90+
execSync(`${packageManager} add ${pkg} --no-lock-file`, { stdio: 'ignore' });
91+
break;
92+
case 'npm':
93+
execSync(`${packageManager} i ${pkg} --no-save`, { stdio: 'ignore' });
94+
break;
95+
}
96+
context.logger.debug(`${pkg} installed successfully.`);
97+
} catch (e) {
98+
context.logger.warn(`Could not install ${pkg}.`, JSON.parse(e));
99+
}
100+
}
101+
102+
export function tryUninstallPackage(context: SchematicContext, packageManager: string, pkg: string) {
103+
try {
104+
context.logger.debug(`Uninstalling ${pkg} via ${packageManager}`);
105+
switch (packageManager) {
106+
case 'yarn':
107+
execSync(`${packageManager} remove ${pkg}`, { stdio: 'ignore' });
108+
break;
109+
case 'npm':
110+
execSync(`${packageManager} uninstall ${pkg} --no-save`, { stdio: 'ignore' });
111+
break;
112+
}
113+
context.logger.debug(`${pkg} uninstalled successfully.`);
114+
} catch (e) {
115+
context.logger
116+
.warn(`Could not uninstall ${pkg}, you may want to uninstall it manually.`, JSON.parse(e));
117+
}
118+
}

projects/igniteui-angular/schematics/utils/dependency-handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ export function addDependencies(options: Options): Rule {
110110
}
111111

112112
/**
113-
* Recursively search for the first property that matches targetProp within the angular.json file.
113+
* Recursively search for the first property that matches targetProp within a json file.
114114
*/
115-
export function getPropertyFromWorkspace(targetProp: string, workspace: any, curKey = ''): any {
115+
export function getPropertyFromWorkspace(targetProp: string, workspace: any, curKey = ''): { key: string, value: any } {
116116
if (workspace.hasOwnProperty(targetProp)) {
117117
return { key: targetProp, value: workspace[targetProp] };
118118
}
@@ -129,7 +129,7 @@ export function getPropertyFromWorkspace(targetProp: string, workspace: any, cur
129129
// If the target property is an object, go one level in.
130130
if (workspace.hasOwnProperty(key)) {
131131
const newValue = getPropertyFromWorkspace(targetProp, workspace[key], key);
132-
if (newValue !== null) {
132+
if (newValue) {
133133
return newValue;
134134
}
135135
}

0 commit comments

Comments
 (0)