feat(schematics): add material scaffolding schematic (#9883)

This commit is contained in:
Austin
2018-02-18 15:24:36 -06:00
committed by Jeremy Elbourn
parent 17255e10df
commit 45399c60d2
13 changed files with 503 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
# Angular Material Schematics
A collection of Schematics for Angular Material.
## Collection
- [Shell](shell/README.md)
+9 -1
View File
@@ -1,5 +1,13 @@
// This is the root config file where the schematics are defined.
{
"$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {}
"schematics": {
// Adds Angular Material to an application without changing any templates
"materialShell": {
"description": "Create a Material shell",
"factory": "./shell",
"schema": "./shell/schema.json",
"aliases": ["material-shell"]
}
}
}
+219
View File
@@ -0,0 +1,219 @@
{
"name": "@angular/material-schematics",
"version": "1.0.23",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@angular-devkit/core": {
"version": "0.0.22",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.0.22.tgz",
"integrity": "sha512-zxrNtTiv60liye/GGeRMnnGgLgAWoqlMTfPLMW0D1qJ4bbrPHtme010mpxS3QL4edcDtQseyXSFCnEkuo2MrRw==",
"requires": {
"source-map": "0.5.7"
}
},
"@angular-devkit/schematics": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.0.42.tgz",
"integrity": "sha512-elTiNL0Nx9oin2pfZTvMBU/d9sgutXaZe8n3xm2p7jfqQZry5MYYFES4hq+WIJjtV/X9gAniafncEpxuF7ikYw==",
"requires": {
"@angular-devkit/core": "0.0.22",
"@ngtools/json-schema": "1.1.0",
"@schematics/schematics": "0.0.11",
"minimist": "1.2.0",
"rxjs": "5.5.6"
}
},
"@ngtools/json-schema": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz",
"integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI="
},
"@schematics/angular": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.1.17.tgz",
"integrity": "sha512-PHE5gk/ogPY/aN94dbbtauHMCq+/7w4Kdcl7tGmSS8mPKEI0wa6XJi//Wq/tHi55lb2fP58oEZU6n6w/wQascw==",
"requires": {
"typescript": "2.6.2"
},
"dependencies": {
"typescript": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
"integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q="
}
}
},
"@schematics/schematics": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.0.11.tgz",
"integrity": "sha512-HAXgAIuuAGjiIKohGlRUkmUTWYtNmclR12KHlQQxT9pHFdEb2OrpHjUp2YoV32jiU6jIZm4pf3ODwlPA0VbwnA=="
},
"@types/jasmine": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz",
"integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==",
"dev": true
},
"@types/node": {
"version": "8.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.2.tgz",
"integrity": "sha512-+IIOUfGkGIUu310djXpOZNR1jHftzr/W7DwoUPiRfzhZWFLXdRt80ePYUjSEYVEs4hJUK4ikXWWo7eHd10RQlA==",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"jasmine": {
"version": "2.99.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.99.0.tgz",
"integrity": "sha1-jKctEC5jm4Z8ZImFbg4YqceqQrc=",
"dev": true,
"requires": {
"exit": "0.1.2",
"glob": "7.1.2",
"jasmine-core": "2.99.1"
}
},
"jasmine-core": {
"version": "2.99.1",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz",
"integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.11"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
"integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
"requires": {
"@types/node": "9.4.4"
},
"dependencies": {
"@types/node": {
"version": "9.4.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.4.tgz",
"integrity": "sha512-pTi6f79uELOTQ2TtXxWcjmJ+iZa1C3ypm6pGNU/viOQX/VfBXLmsZEcJPk1rm+lia+GP6GpgFGUkCvLJ7JOKDA=="
}
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"rxjs": {
"version": "5.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz",
"integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==",
"requires": {
"symbol-observable": "1.0.1"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"symbol-observable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
"integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ="
},
"typescript": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz",
"integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}
+10
View File
@@ -0,0 +1,10 @@
# Material Shell
Adds Angular Material and its depedencies and pre-configures the application.
- Adds Material and CDK to `package.json`
- Adds Material Icons Stylesheet to `index.html`
- Adds Roboto Font to `index.html`
- Ensure `BrowserAnimationsModule` is installed and included in root module
- Adds pre-configured theme to `.angular-cli.json` file OR adds custom theme scaffolding to `styles.scss`
Command: `ng generate material-shell --collection=material-schematics`
+37
View File
@@ -0,0 +1,37 @@
import {AppConfig} from '../utils/devkit-utils/config';
/**
* Create custom theme for the given application configuration.
*/
export function createCustomTheme(app: AppConfig) {
const name = app.name || 'app';
return `
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@import '~@angular/material/theming';
// Plus imports for other components in your app.
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat-core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://www.google.com/design/spec/style/color.html
$${name}-primary: mat-palette($mat-indigo);
$${name}-accent: mat-palette($mat-pink, A200, A100, A400);
// The warn palette is optional (defaults to red).
$${name}-warn: mat-palette($mat-red);
// Create the theme object (a Sass map containing all of the palettes).
$${name}-theme: mat-light-theme($${name}-primary, $${name}-accent, $${name}-warn);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($${name}-theme);
`;
}
+124
View File
@@ -0,0 +1,124 @@
import {
Rule,
SchematicContext,
Tree,
chain,
noop,
SchematicsException
} from '@angular-devkit/schematics';
import {Schema} from './schema';
import {materialVersion, cdkVersion, angularVersion} from '../utils/lib-versions';
import {getConfig, getAppFromConfig, AppConfig, CliConfig} from '../utils/devkit-utils/config';
import {addModuleImportToRootModule} from '../utils/ast';
import {addHeadLink} from '../utils/html';
import {addPackageToPackageJson} from '../utils/package';
import {createCustomTheme} from './custom-theme';
import {normalize} from '@angular-devkit/core';
import {InsertChange} from '../utils/devkit-utils/change';
/**
* Scaffolds the basics of a Angular Material application, this includes:
* - Add Packages to package.json
* - Adds pre-built themes to styles.ext
* - Adds Browser Animation to app.momdule
*/
export default function(options: Schema): Rule {
return chain([
options && options.skipPackageJson ? noop() : addMaterialToPackageJson(options),
addThemeToAppStyles(options),
addAnimationRootConfig(),
addFontsToIndex()
]);
}
/**
* Add material, cdk, annimations to package.json
*/
function addMaterialToPackageJson(options: Schema) {
return (host: Tree) => {
addPackageToPackageJson(host, 'dependencies', '@angular/cdk', cdkVersion);
addPackageToPackageJson(host, 'dependencies', '@angular/material', materialVersion);
addPackageToPackageJson(host, 'dependencies', '@angular/animations', angularVersion);
return host;
};
}
/**
* Add pre-built styles to style.ext file
*/
function addThemeToAppStyles(options: Schema) {
return (host: Tree) => {
const config = getConfig(host);
const themeName = options && options.theme ? options.theme : 'indigo-pink';
const app = getAppFromConfig(config, '0');
if (themeName === 'custom') {
insertCustomTheme(app, host);
} else {
insertPrebuiltTheme(app, host, themeName, config);
}
return host;
};
}
/**
* Insert a custom theme to styles.scss file.
*/
function insertCustomTheme(app: AppConfig, host: Tree) {
const stylesPath = normalize(`/${app.root}/styles.scss`);
const buffer = host.read(stylesPath);
if (!buffer) {
throw new SchematicsException(`Could not find file for path: ${stylesPath}`);
}
const src = buffer.toString();
const insertion = new InsertChange(stylesPath, 0, createCustomTheme(app));
const recorder = host.beginUpdate(stylesPath);
recorder.insertLeft(insertion.pos, insertion.toAdd);
host.commitUpdate(recorder);
}
/**
* Insert a pre-built theme to .angular-cli.json file.
*/
function insertPrebuiltTheme(app: AppConfig, host: Tree, themeName: string, config: CliConfig) {
const themeSrc = `../node_modules/@angular/material/prebuilt-themes/${themeName}.css`;
const hasCurrentTheme = app.styles.find((s: string) => s.indexOf(themeSrc) > -1);
const hasOtherTheme =
app.styles.find((s: string) => s.indexOf('@angular/material/prebuilt-themes') > -1);
if (!hasCurrentTheme && !hasOtherTheme) {
app.styles.splice(0, 0, themeSrc);
}
if (hasOtherTheme) {
throw new SchematicsException(`Another theme is already defined.`);
}
host.overwrite('.angular-cli.json', JSON.stringify(config, null, 2));
}
/**
* Add browser animation module to app.module
*/
function addAnimationRootConfig() {
return (host: Tree) => {
addModuleImportToRootModule(host,
'BrowserAnimationsModule', '@angular/platform-browser/animations');
return host;
};
}
/**
* Adds fonts to the index.ext file
*/
function addFontsToIndex() {
return (host: Tree) => {
addHeadLink(host,
`<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">`);
addHeadLink(host,
`<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">`);
return host;
};
}
+61
View File
@@ -0,0 +1,61 @@
import {Tree} from '@angular-devkit/schematics';
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {join} from 'path';
import {getFileContent} from '@schematics/angular/utility/test';
import {createTestApp} from '../utils/testing';
import {getConfig, getAppFromConfig} from '@schematics/angular/utility/config';
import {getIndexHtmlPath} from '../utils/ast';
import {normalize} from '@angular-devkit/core';
const collectionPath = join(__dirname, '../collection.json');
describe('material-shell-schematic', () => {
let runner: SchematicTestRunner;
let appTree: Tree;
beforeEach(() => {
appTree = createTestApp();
runner = new SchematicTestRunner('schematics', collectionPath);
});
it('should update package.json', () => {
const tree = runner.runSchematic('materialShell', {}, appTree);
const packageJson = JSON.parse(getFileContent(tree, '/package.json'));
expect(packageJson.dependencies['@angular/material']).toBeDefined();
expect(packageJson.dependencies['@angular/cdk']).toBeDefined();
});
it('should add default theme', () => {
const tree = runner.runSchematic('materialShell', {}, appTree);
const config = getConfig(tree);
config.apps.forEach(app => {
expect(app.styles).toContain(
'../node_modules/@angular/material/prebuilt-themes/indigo-pink.css');
});
});
it('should add custom theme', () => {
const tree = runner.runSchematic('materialShell', {
theme: 'custom'
}, appTree);
const config = getConfig(tree);
const app = getAppFromConfig(config, '0');
const stylesPath = normalize(`/${app.root}/styles.scss`);
const buffer = tree.read(stylesPath);
const src = buffer.toString();
expect(src.indexOf(`@import '~@angular/material/theming';`)).toBeGreaterThan(-1);
expect(src.indexOf(`$app-primary`)).toBeGreaterThan(-1);
});
it('should add font links', () => {
const tree = runner.runSchematic('materialShell', {}, appTree);
const indexPath = getIndexHtmlPath(tree);
const buffer = tree.read(indexPath);
const indexSrc = buffer.toString();
expect(indexSrc.indexOf('fonts.googleapis.com')).toBeGreaterThan(-1);
});
});
+11
View File
@@ -0,0 +1,11 @@
export interface Schema {
/**
* Skip package.json install.
*/
skipPackageJson: boolean;
/**
* Name of pre-built theme to install.
*/
theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom';
}
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsMaterialShell",
"title": "Material Shell Options Schema",
"type": "object",
"properties": {
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add materials dependencies to package.json (e.g., --skipPackageJson)"
},
"theme": {
"enum": ["indigo-pink", "deeppurple-amber", "pink-bluegrey", "purple-green", "custom"],
"default": "indigo-pink",
"description": "The theme to apply"
}
},
"required": []
}
+1 -7
View File
@@ -17,11 +17,5 @@
"jasmine",
"node"
]
},
"include": [
"src/**/*"
],
"exclude": [
"src/*/files/**/*"
]
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import {Tree, SchematicsException} from '@angular-devkit/schematics';
import * as parse5 from 'parse5';
import {getIndexPath} from './ast';
import {getIndexHtmlPath} from './ast';
import {InsertChange} from './devkit-utils/change';
/**
@@ -44,7 +44,7 @@ export function getHeadTag(host: Tree, src: string) {
* @param link html element string we are inserting.
*/
export function addHeadLink(host: Tree, link: string) {
const indexPath = getIndexPath(host);
const indexPath = getIndexHtmlPath(host);
const buffer = host.read(indexPath);
if (!buffer) {
throw new SchematicsException(`Could not find file for path: ${indexPath}`);
+3 -3
View File
@@ -1,3 +1,3 @@
export const materialVersion = '^5.0.0';
export const cdkVersion = '^5.0.0';
export const angularVersion = '5.0.0';
export const materialVersion = '^5.2.0';
export const cdkVersion = '^5.2.0';
export const angularVersion = '^5.2.0';
+2 -2
View File
@@ -18,8 +18,8 @@ export function createTestApp() {
viewEncapsulation: 'None',
version: '1.2.3',
routing: true,
style: 'css',
style: 'scss',
skipTests: false,
minimal: false,
});
}
}