Skip to content

Commit 90ba6fb

Browse files
devversionmmalerba
authored andcommitted
refactor(schematics): do not glob for additional stylesheets multiple times (#13155)
* Currently we glob the _whole project_ for additional stylesheets which couldn't be detected by the component walker. This glob runs for each source file multiple times (4 rules; X source files; 4 * x times run the glob). With this change, we only glob once at initialization.
1 parent 25f6c1a commit 90ba6fb

File tree

8 files changed

+84
-62
lines changed

8 files changed

+84
-62
lines changed

src/lib/schematics/update/rules/attribute-selectors/attributeSelectorsStylesheetRule.ts

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

99
import {green, red} from 'chalk';
10-
import {sync as globSync} from 'glob';
1110
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
11+
import * as ts from 'typescript';
1212
import {attributeSelectors} from '../../material/data/attribute-selectors';
1313
import {getChangesForTarget} from '../../material/transform-change-data';
1414
import {ExternalResource} from '../../tslint/component-file';
@@ -18,7 +18,6 @@ import {
1818
createExternalReplacementFailure,
1919
} from '../../tslint/rule-failures';
2020
import {findAllSubstringIndices} from '../../typescript/literal';
21-
import * as ts from 'typescript';
2221

2322
/**
2423
* Rule that walks through every stylesheet in the application and updates outdated
@@ -37,11 +36,8 @@ export class Walker extends ComponentWalker {
3736
data = getChangesForTarget(this.getOptions()[0], attributeSelectors);
3837

3938
constructor(sourceFile: ts.SourceFile, options: IOptions) {
40-
// In some applications, developers will have global stylesheets that are not specified in any
41-
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
42-
// dist and check them as well.
43-
super(sourceFile, options, globSync('!(node_modules|dist)/**/*.+(css|scss)'));
44-
this._reportExtraStylesheetFiles();
39+
super(sourceFile, options);
40+
this._reportExtraStylesheetFiles(options.ruleArguments[1]);
4541
}
4642

4743
visitInlineStylesheet(literal: ts.StringLiteralLike) {

src/lib/schematics/update/rules/css-selectors/cssSelectorsStylesheetRule.ts

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

99
import {green, red} from 'chalk';
10-
import {sync as globSync} from 'glob';
1110
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
1211
import * as ts from 'typescript';
1312
import {cssSelectors} from '../../material/data/css-selectors';
@@ -36,11 +35,8 @@ export class Walker extends ComponentWalker {
3635
data = getChangesForTarget(this.getOptions()[0], cssSelectors);
3736

3837
constructor(sourceFile: ts.SourceFile, options: IOptions) {
39-
// In some applications, developers will have global stylesheets that are not specified in any
40-
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
41-
// dist and check them as well.
42-
super(sourceFile, options, globSync('!(node_modules|dist)/**/*.+(css|scss)'));
43-
this._reportExtraStylesheetFiles();
38+
super(sourceFile, options);
39+
this._reportExtraStylesheetFiles(options.ruleArguments[1]);
4440
}
4541

4642
visitInlineStylesheet(literal: ts.StringLiteralLike) {

src/lib/schematics/update/rules/element-selectors/elementSelectorsStylesheetRule.ts

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

99
import {green, red} from 'chalk';
10-
import {sync as globSync} from 'glob';
1110
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
1211
import * as ts from 'typescript';
1312
import {elementSelectors} from '../../material/data/element-selectors';
@@ -36,11 +35,8 @@ export class Walker extends ComponentWalker {
3635
data = getChangesForTarget(this.getOptions()[0], elementSelectors);
3736

3837
constructor(sourceFile: ts.SourceFile, options: IOptions) {
39-
// In some applications, developers will have global stylesheets that are not specified in any
40-
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
41-
// dist and check them as well.
42-
super(sourceFile, options, globSync('!(node_modules|dist)/**/*.+(css|scss)'));
43-
this._reportExtraStylesheetFiles();
38+
super(sourceFile, options);
39+
this._reportExtraStylesheetFiles(options.ruleArguments[1]);
4440
}
4541

4642
visitInlineStylesheet(literal: ts.StringLiteralLike) {

src/lib/schematics/update/rules/input-names/inputNamesStylesheetRule.ts

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

99
import {green, red} from 'chalk';
10-
import {sync as globSync} from 'glob';
1110
import {IOptions, Replacement, RuleFailure, Rules} from 'tslint';
1211
import * as ts from 'typescript';
1312
import {inputNames} from '../../material/data/input-names';
@@ -41,11 +40,8 @@ export class Walker extends ComponentWalker {
4140
data = getChangesForTarget(this.getOptions()[0], inputNames);
4241

4342
constructor(sourceFile: ts.SourceFile, options: IOptions) {
44-
// In some applications, developers will have global stylesheets that are not specified in any
45-
// Angular component. Therefore we glob up all css and scss files outside of node_modules and
46-
// dist and check them as well.
47-
super(sourceFile, options, globSync('!(node_modules|dist)/**/*.+(css|scss)'));
48-
this._reportExtraStylesheetFiles();
43+
super(sourceFile, options);
44+
this._reportExtraStylesheetFiles(options.ruleArguments[1]);
4945
}
5046

5147
visitInlineStylesheet(literal: ts.StringLiteralLike) {

src/lib/schematics/update/tslint-update.ts renamed to src/lib/schematics/update/tslint-config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ const rulesDirectory = globSync('rules/**/', {cwd: __dirname, absolute: true});
6161
* Creates a TSLint configuration object that can be passed to the schematic `TSLintFixTask`.
6262
* Each rule will have the specified target version as option which can be used to swap out
6363
* the upgrade data based on the given target version.
64+
*
65+
* Additionally we pass a list of additional style files to the TSLint rules because the component
66+
* walker is not able to detect stylesheets which are not referenced by Angular.
6467
*/
65-
export function createTslintConfig(target: TargetVersion) {
68+
export function createTslintConfig(target: TargetVersion, extraStyleFiles: string[]) {
6669
const rules = upgradeRules.reduce((result, ruleName) => {
67-
result[ruleName] = [true, target];
70+
result[ruleName] = [true, target, extraStyleFiles];
6871
return result;
6972
}, {});
7073

src/lib/schematics/update/tslint/component-walker.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,44 @@ describe('ComponentWalker', () => {
118118
expect(() => walker.walk(sourceFile)).not.toThrow();
119119
expect(walker.visitInlineStylesheet).toHaveBeenCalledTimes(2);
120120
});
121+
122+
it('should be able to report additional stylesheet files', () => {
123+
const sourceFile = createSourceFile(``);
124+
const walker = new ComponentWalker(sourceFile, defaultRuleOptions);
125+
const stylesheetPath = './shared-styles/global.css';
126+
127+
spyOn(walker, 'visitExternalStylesheet').and.callFake(node => {
128+
expect(node.getFullText()).toBe('external');
129+
});
130+
131+
mockFs({[stylesheetPath]: 'external'});
132+
133+
walker._reportExtraStylesheetFiles([stylesheetPath]);
134+
135+
expect(walker.visitExternalStylesheet).toHaveBeenCalledTimes(1);
136+
});
137+
138+
it('should not visit stylesheet files multiple times', () => {
139+
const sourceFile = createSourceFile(`
140+
@Component({
141+
styleUrls: ['./my-component.css']
142+
})
143+
export class MyComponent {}
144+
`);
145+
const walker = new ComponentWalker(sourceFile, defaultRuleOptions);
146+
const stylePath = join(dirname(sourceFile.fileName), 'my-component.css');
147+
148+
spyOn(walker, 'visitExternalStylesheet').and.callFake(node => {
149+
expect(node.getFullText()).toBe('external');
150+
});
151+
152+
mockFs({[stylePath]: 'external'});
153+
154+
walker._reportExtraStylesheetFiles([stylePath]);
155+
walker.walk(sourceFile);
156+
157+
expect(walker.visitExternalStylesheet).toHaveBeenCalledTimes(1);
158+
});
121159
});
122160

123161
/** TypeScript source file content that includes a component with inline styles. */

src/lib/schematics/update/tslint/component-walker.ts

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

9-
/**
10-
* TSLint custom walker implementation that also visits external and inline templates.
11-
*/
129
import {existsSync, readFileSync} from 'fs';
13-
import {dirname, join, resolve} from 'path';
14-
import {IOptions, RuleWalker} from 'tslint';
10+
import {dirname, resolve} from 'path';
11+
import {RuleWalker} from 'tslint';
1512
import * as ts from 'typescript';
1613
import {createComponentFile, ExternalResource} from './component-file';
1714

@@ -27,13 +24,12 @@ export class ComponentWalker extends RuleWalker {
2724
visitExternalTemplate(_template: ExternalResource) {}
2825
visitExternalStylesheet(_stylesheet: ExternalResource) {}
2926

30-
private extraFiles: Set<string>;
31-
32-
constructor(sourceFile: ts.SourceFile, options: IOptions, extraFiles: string[] = []) {
33-
super(sourceFile, options);
34-
35-
this.extraFiles = new Set(extraFiles.map(p => resolve(p)));
36-
}
27+
/**
28+
* We keep track of all visited stylesheet files because we allow manually reporting external
29+
* stylesheets which couldn't be detected by the component walker. Reporting these files multiple
30+
* times will result in duplicated TSLint failures and replacements.
31+
*/
32+
private _visitedStylesheetFiles: Set<string> = new Set<string>();
3733

3834
visitNode(node: ts.Node) {
3935
if (node.kind === ts.SyntaxKind.CallExpression) {
@@ -83,12 +79,7 @@ export class ComponentWalker extends RuleWalker {
8379
}
8480

8581
private _reportExternalTemplate(node: ts.StringLiteralLike) {
86-
const templatePath = resolve(join(dirname(this.getSourceFile().fileName), node.text));
87-
88-
// Do not report the specified additional files multiple times.
89-
if (this.extraFiles.has(templatePath)) {
90-
return;
91-
}
82+
const templatePath = resolve(dirname(this.getSourceFile().fileName), node.text);
9283

9384
// Check if the external template file exists before proceeding.
9485
if (!existsSync(templatePath)) {
@@ -113,12 +104,7 @@ export class ComponentWalker extends RuleWalker {
113104
private _visitExternalStylesArrayLiteral(expression: ts.ArrayLiteralExpression) {
114105
expression.elements.forEach(node => {
115106
if (ts.isStringLiteralLike(node)) {
116-
const stylePath = resolve(join(dirname(this.getSourceFile().fileName), node.text));
117-
118-
// Do not report the specified additional files multiple times.
119-
if (this.extraFiles.has(stylePath)) {
120-
return;
121-
}
107+
const stylePath = resolve(dirname(this.getSourceFile().fileName), node.text);
122108

123109
// Check if the external stylesheet file exists before proceeding.
124110
if (!existsSync(stylePath)) {
@@ -130,20 +116,22 @@ export class ComponentWalker extends RuleWalker {
130116
});
131117
}
132118

133-
_reportExternalStyle(stylePath: string) {
119+
private _reportExternalStyle(stylePath: string) {
120+
// Keep track of all reported external stylesheets because we allow reporting additional
121+
// stylesheet files which couldn't be detected by the component walker. This allows us to
122+
// ensure that no stylesheet files are visited multiple times.
123+
if (this._visitedStylesheetFiles.has(stylePath)) {
124+
return;
125+
}
126+
127+
this._visitedStylesheetFiles.add(stylePath);
128+
134129
// Create a fake TypeScript source file that includes the stylesheet content.
135130
const stylesheetFile = createComponentFile(stylePath, readFileSync(stylePath, 'utf8'));
136131

137132
this.visitExternalStylesheet(stylesheetFile);
138133
}
139134

140-
/** Reports all extra files that have been specified at initialization. */
141-
// TODO(devversion): this should be done automatically but deferred because
142-
// the base class "data" property member is not ready at initialization.
143-
_reportExtraStylesheetFiles() {
144-
this.extraFiles.forEach(file => this._reportExternalStyle(file));
145-
}
146-
147135
/**
148136
* Recursively searches for the metadata object literal expression inside of a directive call
149137
* expression. Since expression calls can be nested through *parenthesized* expressions, we
@@ -169,4 +157,9 @@ export class ComponentWalker extends RuleWalker {
169157
this.addFailureAtNode(node, `Could not resolve resource file: "${resourceUrl}". ` +
170158
`Skipping automatic upgrade for this file.`);
171159
}
160+
161+
/** Reports the specified additional stylesheets. */
162+
_reportExtraStylesheetFiles(filePaths: string[]) {
163+
filePaths.forEach(filePath => this._reportExternalStyle(resolve(filePath)));
164+
}
172165
}

src/lib/schematics/update/update.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import {Rule, SchematicContext, TaskId, Tree} from '@angular-devkit/schematics';
1010
import {RunSchematicTask, TslintFixTask} from '@angular-devkit/schematics/tasks';
11+
import {sync as globSync} from 'glob';
1112
import {TargetVersion} from './index';
1213
import {getProjectTsConfigPaths} from './project-tsconfig-paths';
13-
import {createTslintConfig} from './tslint-update';
14+
import {createTslintConfig} from './tslint-config';
1415

1516
/** Entry point for `ng update` from Angular CLI. */
1617
export function createUpdateRule(targetVersion: TargetVersion): Rule {
@@ -23,8 +24,11 @@ export function createUpdateRule(targetVersion: TargetVersion): Rule {
2324
throw new Error('Could not find any tsconfig file. Please submit an issue on the Angular ' +
2425
'Material repository that includes the name of your TypeScript configuration.');
2526
}
26-
27-
const tslintConfig = createTslintConfig(targetVersion);
27+
// In some applications, developers will have global stylesheets which are not specified in any
28+
// Angular component. Therefore we glob up all CSS and SCSS files outside of node_modules and
29+
// dist. The files will be read by the individual stylesheet rules and checked.
30+
const extraStyleFiles = globSync('!(node_modules|dist)/**/*.+(css|scss)', {absolute: true});
31+
const tslintConfig = createTslintConfig(targetVersion, extraStyleFiles);
2832

2933
for (const tsconfig of projectTsConfigPaths) {
3034
// Run the update tslint rules.

0 commit comments

Comments
 (0)