Skip to content

Commit 5770507

Browse files
committed
fix(ng-add): do not incorrectly insert custom-theme into CSS files
* Currently the schematics add the custom-theme SCSS code to any project style file (`styles.EXT`). This is incorrect because the SCSS needs to be placed inside of a SCSS file. Instead of throwing an error we just create a new SCSS file with the theme and add it to the workspace config. * Aligns the global font-family statement with the current Angular Material typography font-family. * Adds missing tests for the global app styles. * Removes an accidentally committed `console.log()`.
1 parent b9651df commit 5770507

File tree

4 files changed

+99
-45
lines changed

4 files changed

+99
-45
lines changed

src/lib/schematics/install/index.spec.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {Tree} from '@angular-devkit/schematics';
22
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
3+
import {getProjectStyleFile} from '@angular/material/schematics/utils/project-style-file';
34
import {getIndexHtmlPath} from './fonts/project-index-html';
45
import {getProjectFromWorkspace} from '../utils/get-project';
56
import {getFileContent} from '@schematics/angular/utility/test';
67
import {collectionPath, createTestApp} from '../test-setup/test-app';
7-
import {getWorkspace} from '@schematics/angular/utility/config';
8+
import {getWorkspace, WorkspaceProject} from '@schematics/angular/utility/config';
89
import {normalize} from '@angular-devkit/core';
910

1011
describe('material-install-schematic', () => {
@@ -16,6 +17,14 @@ describe('material-install-schematic', () => {
1617
runner = new SchematicTestRunner('schematics', collectionPath);
1718
});
1819

20+
/** Expects the given file to be in the styles of the specified workspace project. */
21+
function expectProjectStyleFile(project: WorkspaceProject, filePath: string) {
22+
expect(project.architect!['build']).toBeTruthy();
23+
expect(project.architect!['build']['options']).toBeTruthy();
24+
expect(project.architect!['build']['options']['styles']).toContain(
25+
filePath, `Expected "${filePath}" to be added to the project styles in the workspace.`);
26+
}
27+
1928
it('should update package.json', () => {
2029
const tree = runner.runSchematic('ng-add', {}, appTree);
2130
const packageJson = JSON.parse(getFileContent(tree, '/package.json'));
@@ -33,17 +42,11 @@ describe('material-install-schematic', () => {
3342
const workspace = getWorkspace(tree);
3443
const project = getProjectFromWorkspace(workspace);
3544

36-
console.log(tree.files);
37-
38-
expect(project.architect!['build']).toBeTruthy();
39-
expect(project.architect!['build']['options']).toBeTruthy();
40-
expect(project.architect!['build']['options']['styles']).toContain(
41-
'./node_modules/@angular/material/prebuilt-themes/indigo-pink.css');
45+
expectProjectStyleFile(project,
46+
'./node_modules/@angular/material/prebuilt-themes/indigo-pink.css');
4247
});
4348

4449
it('should support adding a custom theme', () => {
45-
// TODO(devversion): currently a "custom" theme does only work for projects using SCSS.
46-
// TODO(devversion): Throw an error if a custom theme is being installed in a CSS project.
4750
appTree = createTestApp({style: 'scss'});
4851

4952
const tree = runner.runSchematic('ng-add', {theme: 'custom'}, appTree);
@@ -59,6 +62,19 @@ describe('material-install-schematic', () => {
5962
expect(src.indexOf(`$app-primary`)).toBeGreaterThan(-1);
6063
});
6164

65+
it('should create a custom theme file if no SCSS file could be found', () => {
66+
appTree = createTestApp({style: 'css'});
67+
68+
const tree = runner.runSchematic('ng-add', {theme: 'custom'}, appTree);
69+
70+
const workspace = getWorkspace(tree);
71+
const project = getProjectFromWorkspace(workspace);
72+
const expectedStylesPath = normalize(`/${project.root}/src/custom-theme.scss`);
73+
74+
expect(tree.files).toContain(expectedStylesPath, 'Expected a custom theme file to be created');
75+
expectProjectStyleFile(project, 'projects/material/src/custom-theme.scss');
76+
});
77+
6278
it('should add font links', () => {
6379
const tree = runner.runSchematic('ng-add', {}, appTree);
6480
const workspace = getWorkspace(tree);
@@ -76,4 +92,17 @@ describe('material-install-schematic', () => {
7692
expect(htmlContent).toContain(
7793
' <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"');
7894
});
95+
96+
it('should add material app styles', () => {
97+
const tree = runner.runSchematic('ng-add', {}, appTree);
98+
const workspace = getWorkspace(tree);
99+
const project = getProjectFromWorkspace(workspace);
100+
101+
const defaultStylesPath = getProjectStyleFile(project);
102+
const htmlContent = tree.read(defaultStylesPath)!.toString();
103+
104+
expect(htmlContent).toContain('html, body { height: 100%; }');
105+
expect(htmlContent).toContain(
106+
'body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }');
107+
});
79108
});

src/lib/schematics/install/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function addMaterialAppStyles(options: Schema) {
9999
const htmlContent = buffer.toString();
100100
const insertion = '\n' +
101101
`html, body { height: 100%; }\n` +
102-
`body { margin: 0; font-family: 'Roboto', sans-serif; }\n`;
102+
`body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }\n`;
103103

104104
if (htmlContent.includes(insertion)) {
105105
return;

src/lib/schematics/install/theming/theming.ts

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

9+
import {normalize} from '@angular-devkit/core';
910
import {SchematicsException, Tree} from '@angular-devkit/schematics';
1011
import {InsertChange} from '@schematics/angular/utility/change';
1112
import {getWorkspace, WorkspaceProject, WorkspaceSchema} from '@schematics/angular/utility/config';
13+
import {join} from 'path';
1214
import {getProjectFromWorkspace} from '../../utils/get-project';
1315
import {getProjectStyleFile} from '../../utils/project-style-file';
1416
import {Schema} from '../schema';
@@ -20,14 +22,14 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree {
2022
return function(host: Tree): Tree {
2123
const workspace = getWorkspace(host);
2224
const project = getProjectFromWorkspace(workspace, options.project);
25+
const themeName = options.theme || 'indigo-pink';
2326

2427
// Because the build setup for the Angular CLI can be changed so dramatically, we can't know
2528
// where to generate anything if the project is not using the default config for build and test.
26-
assertDefaultProjectConfig(project);
29+
assertDefaultBuildersConfigured(project);
2730

28-
const themeName = options.theme || 'indigo-pink';
2931
if (themeName === 'custom') {
30-
insertCustomTheme(project, options.project, host);
32+
insertCustomTheme(project, options.project, host, workspace);
3133
} else {
3234
insertPrebuiltTheme(project, host, themeName, workspace, options.project);
3335
}
@@ -36,19 +38,31 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree {
3638
};
3739
}
3840

39-
/** Insert a custom theme to styles.scss file. */
40-
function insertCustomTheme(project: WorkspaceProject, projectName: string, host: Tree) {
41-
const stylesPath = getProjectStyleFile(project);
42-
const buffer = host.read(stylesPath);
41+
/**
42+
* Insert a custom theme to project style file. If no valid style file could be found, a new
43+
* Scss file for the custom theme will be created.
44+
*/
45+
function insertCustomTheme(project: WorkspaceProject, projectName: string, host: Tree,
46+
workspace: WorkspaceSchema) {
4347

44-
if (buffer) {
45-
const insertion = new InsertChange(stylesPath, 0, createCustomTheme(projectName));
46-
const recorder = host.beginUpdate(stylesPath);
47-
recorder.insertLeft(insertion.pos, insertion.toAdd);
48-
host.commitUpdate(recorder);
49-
} else {
50-
console.warn(`Skipped custom theme; could not find file: ${stylesPath}`);
48+
const stylesPath = getProjectStyleFile(project, 'scss');
49+
const themeContent = createCustomTheme(projectName);
50+
51+
if (!stylesPath) {
52+
// Normalize the path through the devkit utilities because we want to avoid having
53+
// unnecessary path segments and windows backslash delimiters.
54+
const customThemePath = normalize(join(project.sourceRoot, 'custom-theme.scss'));
55+
56+
host.create(customThemePath, themeContent);
57+
addStyleToTarget(project.architect['build'], host, customThemePath, workspace);
58+
return;
5159
}
60+
61+
const insertion = new InsertChange(stylesPath, 0, themeContent);
62+
const recorder = host.beginUpdate(stylesPath);
63+
64+
recorder.insertLeft(insertion.pos, insertion.toAdd);
65+
host.commitUpdate(recorder);
5266
}
5367

5468
/** Insert a pre-built theme into the angular.json file. */
@@ -87,23 +101,20 @@ function addStyleToTarget(target: any, host: Tree, asset: string, workspace: Wor
87101
host.overwrite('angular.json', JSON.stringify(workspace, null, 2));
88102
}
89103

90-
/** Throws if the project is not using the default build and test config. */
91-
function assertDefaultProjectConfig(project: WorkspaceProject) {
92-
if (!isProjectUsingDefaultConfig(project)) {
93-
throw new SchematicsException('Your project is not using the default configuration for ' +
94-
'build and test. The Angular Material schematics can only be used with the default ' +
95-
'configuration');
96-
}
97-
}
98-
99-
/** Gets whether the Angular CLI project is using the default build configuration. */
100-
function isProjectUsingDefaultConfig(project: WorkspaceProject) {
104+
/** Throws if the project is not using the default Angular devkit builders. */
105+
function assertDefaultBuildersConfigured(project: WorkspaceProject) {
101106
const defaultBuilder = '@angular-devkit/build-angular:browser';
102107
const defaultTestBuilder = '@angular-devkit/build-angular:karma';
103108

104-
return project.architect &&
109+
const hasDefaultBuilders = project.architect &&
105110
project.architect['build'] &&
106111
project.architect['build']['builder'] === defaultBuilder &&
107112
project.architect['test'] &&
108113
project.architect['test']['builder'] === defaultTestBuilder;
114+
115+
if (!hasDefaultBuilders) {
116+
throw new SchematicsException(
117+
'Your project is not using the default builders for build and test. The Angular Material ' +
118+
'schematics can only be used if the original builders from the Angular CLI are configured.');
119+
}
109120
}

src/lib/schematics/utils/project-style-file.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,43 @@
77
*/
88

99
import {normalize} from '@angular-devkit/core';
10-
import {SchematicsException} from '@angular-devkit/schematics';
1110
import {WorkspaceProject} from '@schematics/angular/utility/config';
1211

13-
/** Looks for the primary style file for a given project and returns its path. */
14-
export function getProjectStyleFile(project: WorkspaceProject): string {
12+
/** Regular expression that matches all possible Angular CLI default style files. */
13+
const defaultStyleFileRegex = /styles\.(c|le|sc)ss/;
14+
15+
/** Regular expression that matches all files that have a proper stylesheet extension. */
16+
const validStyleFileRegex = /\.(c|le|sc)ss/;
17+
18+
/**
19+
* Gets a style file with the given extension in a project and returns its path. If no
20+
* extension is specified, any style file with a valid extension will be returned.
21+
*/
22+
export function getProjectStyleFile(project: WorkspaceProject, extension?: string): string | null {
1523
const buildTarget = project.architect['build'];
1624

1725
if (buildTarget.options && buildTarget.options.styles && buildTarget.options.styles.length) {
1826
const styles = buildTarget.options.styles.map(s => typeof s === 'string' ? s : s.input);
1927

20-
// First, see if any of the assets is called "styles.(le|sc|c)ss", which is the default
21-
// "main" style sheet.
22-
const defaultMainStylePath = styles.find(a => /styles\.(c|le|sc)ss/.test(a));
28+
// Look for the default style file that is generated for new projects by the Angular CLI. This
29+
// default style file is usually called `styles.ext` unless it has been changed explicitly.
30+
const defaultMainStylePath = styles
31+
.find(file => extension ? file === `styles.${extension}` : defaultStyleFileRegex.test(file));
32+
2333
if (defaultMainStylePath) {
2434
return normalize(defaultMainStylePath);
2535
}
2636

27-
// If there was no obvious default file, use the first style asset.
28-
const fallbackStylePath = styles.find(a => /\.(c|le|sc)ss/.test(a));
37+
// If no default style file could be found, use the first style file that matches the given
38+
// extension. If no extension specified explicitly, we look for any file with a valid style
39+
// file extension.
40+
const fallbackStylePath = styles
41+
.find(file => extension ? file.endsWith(`.${extension}`) : validStyleFileRegex.test(file));
42+
2943
if (fallbackStylePath) {
3044
return normalize(fallbackStylePath);
3145
}
3246
}
3347

34-
throw new SchematicsException('No style files could be found into which a theme could be added.');
48+
return null;
3549
}

0 commit comments

Comments
 (0)