Skip to content

Commit 1cb7099

Browse files
devversionmmalerba
authored andcommitted
fix(ng-update): avoid error if project has folder ending with… (#18454)
Previously, we always queried for `.css` and `.scss` files in the whole workspace. We relied on `glob` for this, but did not disable folder matching. Hence, it could happen that in some cases the migration rule tried reading content from a directory (resulting in `EISDIR` error). Additionally, we always queried for files in the actual workspace root. This has downsides and is not correct because it could mean that stylesheets from other projects are accidentally read (in case of a monorepo for example). Fixes #18434.
1 parent b61582c commit 1cb7099

File tree

6 files changed

+92
-81
lines changed

6 files changed

+92
-81
lines changed

src/cdk/schematics/ng-update/test-cases/misc/global-stylesheets.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('global stylesheets migration', () => {
1414
// be picked up by the update-tool.
1515
writeFile(testStylesheetPath,
1616
readFileSync(require.resolve('./global-stylesheets-test.scss'), 'utf8'));
17+
writeFile('/projects/cdk-testing/third_party/materialize.css/bundle.css', '');
1718

1819
await runFixers();
1920

@@ -24,4 +25,27 @@ describe('global stylesheets migration', () => {
2425

2526
removeTempDir();
2627
});
28+
29+
it('should not check stylesheets outside of project target', async () => {
30+
const {runFixers, writeFile, removeTempDir, appTree} = await createTestCaseSetup(
31+
'migration-v6', migrationCollection, []);
32+
const subProjectStylesheet = '[cdkPortalHost] {\n color: red;\n}\n';
33+
34+
writeFile('/sub_project/node_modules/materialize.css/package.json', '');
35+
writeFile('/sub_project/assets/test.css', subProjectStylesheet);
36+
37+
let error: any = null;
38+
try {
39+
await runFixers()
40+
} catch (e) {
41+
error = e;
42+
}
43+
44+
expect(error).toBeNull();
45+
// if the external stylesheet that is not of a project target would have been checked
46+
// by accident, the stylesheet would differ from the original file content.
47+
expect(appTree.readContent('/sub_project/assets/test.css')).toBe(subProjectStylesheet);
48+
49+
removeTempDir();
50+
});
2751
});

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

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88

99
import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
1010
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
11+
import {WorkspaceProject} from '@schematics/angular/utility/workspace-models';
1112

1213
import {MigrationRuleType, runMigrationRules} from '../../update-tool';
1314
import {TargetVersion} from '../../update-tool/target-version';
14-
import {getProjectTsConfigPaths} from '../../utils/project-tsconfig-paths';
15+
import {
16+
getTargetTsconfigPath,
17+
getWorkspaceConfigGracefully
18+
} from '../../utils/project-tsconfig-paths';
1519
import {RuleUpgradeData} from '../upgrade-data';
1620

1721
import {AttributeSelectorsRule} from './attribute-selectors-rule';
@@ -44,8 +48,8 @@ export const cdkMigrationRules: MigrationRuleType<RuleUpgradeData>[] = [
4448

4549
type NullableMigrationRule = MigrationRuleType<RuleUpgradeData|null>;
4650

47-
type PostMigrationFn = (context: SchematicContext, targetVersion: TargetVersion,
48-
hasFailure: boolean) => void;
51+
type PostMigrationFn =
52+
(context: SchematicContext, targetVersion: TargetVersion, hasFailure: boolean) => void;
4953

5054
/**
5155
* Creates a Angular schematic rule that runs the upgrade for the
@@ -56,35 +60,47 @@ export function createUpgradeRule(
5660
onMigrationCompleteFn?: PostMigrationFn): Rule {
5761
return async (tree: Tree, context: SchematicContext) => {
5862
const logger = context.logger;
59-
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
63+
const workspace = getWorkspaceConfigGracefully(tree);
6064

61-
if (!buildPaths.length && !testPaths.length) {
62-
// We don't want to throw here because it would mean that other migrations in the
63-
// pipeline don't run either. Rather print an error message.
64-
logger.error('Could not find any TypeScript project in the CLI workspace configuration.');
65+
if (workspace === null) {
66+
logger.error('Could not find workspace configuration file.');
6567
return;
6668
}
6769

6870
// Keep track of all project source files which have been checked/migrated. This is
6971
// necessary because multiple TypeScript projects can contain the same source file and
7072
// we don't want to check these again, as this would result in duplicated failure messages.
7173
const analyzedFiles = new Set<string>();
74+
const projectNames = Object.keys(workspace.projects);
7275
const rules = [...cdkMigrationRules, ...extraRules];
7376
let hasRuleFailures = false;
7477

75-
const runMigration = (tsconfigPath: string, isTestTarget: boolean) => {
76-
const result = runMigrationRules(
77-
tree, context.logger, tsconfigPath, isTestTarget, targetVersion,
78-
rules, upgradeData, analyzedFiles);
79-
80-
hasRuleFailures = hasRuleFailures || result.hasFailures;
81-
};
82-
83-
buildPaths.forEach(p => runMigration(p, false));
84-
testPaths.forEach(p => runMigration(p, true));
78+
const runMigration =
79+
(project: WorkspaceProject, tsconfigPath: string, isTestTarget: boolean) => {
80+
const result = runMigrationRules(
81+
project, tree, context.logger, tsconfigPath, isTestTarget, targetVersion, rules,
82+
upgradeData, analyzedFiles);
83+
hasRuleFailures = hasRuleFailures || result.hasFailures;
84+
};
85+
86+
for (const projectName of projectNames) {
87+
const project = workspace.projects[projectName];
88+
const buildTsconfigPath = getTargetTsconfigPath(project, 'build');
89+
const testTsconfigPath = getTargetTsconfigPath(project, 'test');
90+
91+
if (!buildTsconfigPath && !testTsconfigPath) {
92+
logger.warn(`Could not find TypeScript project for project: ${projectName}`);
93+
continue;
94+
}
95+
if (buildTsconfigPath !== null) {
96+
runMigration(project, buildTsconfigPath, false);
97+
}
98+
if (testTsconfigPath !== null) {
99+
runMigration(project, testTsconfigPath, true);
100+
}
101+
}
85102

86103
let runPackageManager = false;
87-
88104
// Run the global post migration static members for all migration rules.
89105
rules.forEach(rule => {
90106
const actionResult = rule.globalPostMigration(tree, context);

src/cdk/schematics/update-tool/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ts_library(
1010
deps = [
1111
"@npm//@angular-devkit/core",
1212
"@npm//@angular-devkit/schematics",
13+
"@npm//@schematics/angular",
1314
"@npm//@types/glob",
1415
"@npm//@types/node",
1516
"@npm//typescript",

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

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

99
import {logging, normalize} from '@angular-devkit/core';
1010
import {Tree, UpdateRecorder} from '@angular-devkit/schematics';
11+
import {WorkspaceProject} from '@schematics/angular/utility/workspace-models';
1112
import {sync as globSync} from 'glob';
12-
import {dirname, relative} from 'path';
13+
import {dirname, join, relative} from 'path';
1314
import * as ts from 'typescript';
1415

1516
import {ComponentResourceCollector} from './component-resource-collector';
@@ -18,19 +19,20 @@ import {TargetVersion} from './target-version';
1819
import {parseTsconfigFile} from './utils/parse-tsconfig';
1920

2021
export type Constructor<T> = (new (...args: any[]) => T);
21-
export type MigrationRuleType<T> = Constructor<MigrationRule<T>>
22-
& {[m in keyof typeof MigrationRule]: (typeof MigrationRule)[m]};
22+
export type MigrationRuleType<T> =
23+
Constructor<MigrationRule<T>>&{[m in keyof typeof MigrationRule]: (typeof MigrationRule)[m]};
2324

2425

2526
export function runMigrationRules<T>(
26-
tree: Tree, logger: logging.LoggerApi, tsconfigPath: string, isTestTarget: boolean,
27-
targetVersion: TargetVersion, ruleTypes: MigrationRuleType<T>[], upgradeData: T,
28-
analyzedFiles: Set<string>): {hasFailures: boolean} {
27+
project: WorkspaceProject, tree: Tree, logger: logging.LoggerApi, tsconfigPath: string,
28+
isTestTarget: boolean, targetVersion: TargetVersion, ruleTypes: MigrationRuleType<T>[],
29+
upgradeData: T, analyzedFiles: Set<string>): {hasFailures: boolean} {
2930
// The CLI uses the working directory as the base directory for the
3031
// virtual file system tree.
3132
const basePath = process.cwd();
3233
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
3334
const host = ts.createCompilerHost(parsed.options, true);
35+
const projectFsPath = join(basePath, project.root);
3436

3537
// We need to overwrite the host "readFile" method, as we want the TypeScript
3638
// program to be based on the file contents in the virtual file tree.
@@ -94,8 +96,9 @@ export function runMigrationRules<T>(
9496
// In some applications, developers will have global stylesheets which are not specified in any
9597
// Angular component. Therefore we glob up all CSS and SCSS files outside of node_modules and
9698
// dist. The files will be read by the individual stylesheet rules and checked.
97-
// TODO(devversion): double-check if we can solve this in a more elegant way.
98-
globSync('!(node_modules|dist)/**/*.+(css|scss)', {absolute: true, cwd: basePath})
99+
// TODO: rework this to collect external/global stylesheets from the workspace config. COMP-280.
100+
globSync(
101+
'!(node_modules|dist)/**/*.+(css|scss)', {absolute: true, cwd: projectFsPath, nodir: true})
99102
.filter(filePath => !resourceCollector.resolvedStylesheets.some(s => s.filePath === filePath))
100103
.forEach(filePath => {
101104
const stylesheet = resourceCollector.resolveExternalStylesheet(filePath, null);

src/cdk/schematics/utils/project-tsconfig-paths.spec.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {HostTree} from '@angular-devkit/schematics';
22
import {UnitTestTree} from '@angular-devkit/schematics/testing';
3-
import {getProjectTsConfigPaths} from './project-tsconfig-paths';
3+
import {getTargetTsconfigPath, getWorkspaceConfigGracefully} from './project-tsconfig-paths';
44

55
describe('project tsconfig paths', () => {
66
let testTree: UnitTestTree;
@@ -14,7 +14,10 @@ describe('project tsconfig paths', () => {
1414
{my_name: {architect: {build: {options: {tsConfig: './my-custom-config.json'}}}}}
1515
}));
1616

17-
expect(getProjectTsConfigPaths(testTree).buildPaths).toEqual(['my-custom-config.json']);
17+
const config = getWorkspaceConfigGracefully(testTree);
18+
expect(config).not.toBeNull();
19+
expect(getTargetTsconfigPath(config!.projects['my_name'], 'build'))
20+
.toEqual('my-custom-config.json');
1821
});
1922

2023
it('should be able to read workspace configuration which is using JSON5 features', () => {
@@ -34,7 +37,10 @@ describe('project tsconfig paths', () => {
3437
},
3538
}`);
3639

37-
expect(getProjectTsConfigPaths(testTree).buildPaths).toEqual(['my-build-config.json']);
40+
const config = getWorkspaceConfigGracefully(testTree);
41+
expect(config).not.toBeNull();
42+
expect(getTargetTsconfigPath(config!.projects['with_tests'], 'build'))
43+
.toEqual('my-build-config.json');
3844
});
3945

4046
it('should detect test tsconfig path inside of angular.json file', () => {
@@ -43,7 +49,10 @@ describe('project tsconfig paths', () => {
4349
projects: {my_name: {architect: {test: {options: {tsConfig: './my-test-config.json'}}}}}
4450
}));
4551

46-
expect(getProjectTsConfigPaths(testTree).testPaths).toEqual(['my-test-config.json']);
52+
const config = getWorkspaceConfigGracefully(testTree);
53+
expect(config).not.toBeNull();
54+
expect(getTargetTsconfigPath(config!.projects['my_name'], 'test'))
55+
.toEqual('my-test-config.json');
4756
});
4857

4958
it('should detect test tsconfig path inside of .angular.json file', () => {
@@ -53,15 +62,9 @@ describe('project tsconfig paths', () => {
5362
{with_tests: {architect: {test: {options: {tsConfig: './my-test-config.json'}}}}}
5463
}));
5564

56-
expect(getProjectTsConfigPaths(testTree).testPaths).toEqual(['my-test-config.json']);
57-
});
58-
59-
it('should not return duplicate tsconfig files', () => {
60-
testTree.create('/tsconfig.json', '');
61-
testTree.create('/.angular.json', JSON.stringify({
62-
projects: {app: {architect: {build: {options: {tsConfig: 'tsconfig.json'}}}}}
63-
}));
64-
65-
expect(getProjectTsConfigPaths(testTree).buildPaths).toEqual(['tsconfig.json']);
65+
const config = getWorkspaceConfigGracefully(testTree);
66+
expect(config).not.toBeNull();
67+
expect(getTargetTsconfigPath(config!.projects['with_tests'], 'test'))
68+
.toEqual('my-test-config.json');
6669
});
6770
});

src/cdk/schematics/utils/project-tsconfig-paths.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,13 @@
88

99
import {JsonParseMode, normalize, parseJson} from '@angular-devkit/core';
1010
import {Tree} from '@angular-devkit/schematics';
11-
import {WorkspaceProject} from '@schematics/angular/utility/workspace-models';
11+
import {WorkspaceProject, WorkspaceSchema} from '@schematics/angular/utility/workspace-models';
1212

1313
/** Name of the default Angular CLI workspace configuration files. */
1414
const defaultWorkspaceConfigPaths = ['/angular.json', '/.angular.json'];
1515

16-
/**
17-
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
18-
* and looking for common tsconfig locations.
19-
*/
20-
export function getProjectTsConfigPaths(tree: Tree): {buildPaths: string[], testPaths: string[]} {
21-
// Start with some tsconfig paths that are generally used within CLI projects. Note
22-
// that we are not interested in IDE-specific tsconfig files (e.g. /tsconfig.json)
23-
const buildPaths = new Set<string>([]);
24-
const testPaths = new Set<string>([]);
25-
26-
// Add any tsconfig directly referenced in a build or test task of the angular.json workspace.
27-
const workspace = getWorkspaceConfigGracefully(tree);
28-
29-
if (workspace) {
30-
const projects = Object.keys(workspace.projects).map(name => workspace.projects[name]);
31-
for (const project of projects) {
32-
const buildPath = getTargetTsconfigPath(project, 'build');
33-
const testPath = getTargetTsconfigPath(project, 'test');
34-
35-
if (buildPath) {
36-
buildPaths.add(buildPath);
37-
}
38-
39-
if (testPath) {
40-
testPaths.add(testPath);
41-
}
42-
}
43-
}
44-
45-
// Filter out tsconfig files that don't exist in the CLI project.
46-
return {
47-
buildPaths: Array.from(buildPaths).filter(p => tree.exists(p)),
48-
testPaths: Array.from(testPaths).filter(p => tree.exists(p)),
49-
};
50-
}
51-
5216
/** Gets the tsconfig path from the given target within the specified project. */
53-
function getTargetTsconfigPath(project: WorkspaceProject, targetName: string): string|null {
17+
export function getTargetTsconfigPath(project: WorkspaceProject, targetName: string): string|null {
5418
if (project.targets && project.targets[targetName] && project.targets[targetName].options &&
5519
project.targets[targetName].options.tsConfig) {
5620
return normalize(project.targets[targetName].options.tsConfig);
@@ -69,7 +33,7 @@ function getTargetTsconfigPath(project: WorkspaceProject, targetName: string): s
6933
* versions of the CLI. Also it's important to resolve the workspace gracefully because
7034
* the CLI project could be still using `.angular-cli.json` instead of thew new config.
7135
*/
72-
function getWorkspaceConfigGracefully(tree: Tree): any {
36+
export function getWorkspaceConfigGracefully(tree: Tree): null|WorkspaceSchema {
7337
const path = defaultWorkspaceConfigPaths.find(filePath => tree.exists(filePath));
7438
const configBuffer = tree.read(path!);
7539

@@ -80,7 +44,7 @@ function getWorkspaceConfigGracefully(tree: Tree): any {
8044
try {
8145
// Parse the workspace file as JSON5 which is also supported for CLI
8246
// workspace configurations.
83-
return parseJson(configBuffer.toString(), JsonParseMode.Json5);
47+
return parseJson(configBuffer.toString(), JsonParseMode.Json5) as unknown as WorkspaceSchema;
8448
} catch (e) {
8549
return null;
8650
}

0 commit comments

Comments
 (0)