diff --git a/schematics/README.md b/schematics/README.md new file mode 100644 index 000000000000..aa82c497fbdc --- /dev/null +++ b/schematics/README.md @@ -0,0 +1,5 @@ +# Angular Material Schematics +A collection of Schematics for Angular Material. + +## Collection +- [Shell](shell/README.md) diff --git a/schematics/collection.json b/schematics/collection.json index 7768476c5e4c..5f482db720b3 100644 --- a/schematics/collection.json +++ b/schematics/collection.json @@ -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"] + } + } } diff --git a/schematics/package-lock.json b/schematics/package-lock.json new file mode 100644 index 000000000000..c48401ccbad4 --- /dev/null +++ b/schematics/package-lock.json @@ -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 + } + } +} diff --git a/schematics/shell/README.md b/schematics/shell/README.md new file mode 100644 index 000000000000..4b3942fc113f --- /dev/null +++ b/schematics/shell/README.md @@ -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` diff --git a/schematics/shell/custom-theme.ts b/schematics/shell/custom-theme.ts new file mode 100644 index 000000000000..b72b9c66d087 --- /dev/null +++ b/schematics/shell/custom-theme.ts @@ -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); + +`; +} diff --git a/schematics/shell/index.ts b/schematics/shell/index.ts new file mode 100644 index 000000000000..7c8b1d4c4f85 --- /dev/null +++ b/schematics/shell/index.ts @@ -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, + ``); + addHeadLink(host, + ``); + return host; + }; +} diff --git a/schematics/shell/index_spec.ts b/schematics/shell/index_spec.ts new file mode 100644 index 000000000000..3e6ff8eb1cd8 --- /dev/null +++ b/schematics/shell/index_spec.ts @@ -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); + }); +}); diff --git a/schematics/shell/schema.d.ts b/schematics/shell/schema.d.ts new file mode 100644 index 000000000000..4abd4acea70f --- /dev/null +++ b/schematics/shell/schema.d.ts @@ -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'; +} diff --git a/schematics/shell/schema.json b/schematics/shell/schema.json new file mode 100644 index 000000000000..a6503eba7e93 --- /dev/null +++ b/schematics/shell/schema.json @@ -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": [] +} diff --git a/schematics/tsconfig.json b/schematics/tsconfig.json index 219cec2afd28..c27cde63131a 100644 --- a/schematics/tsconfig.json +++ b/schematics/tsconfig.json @@ -17,11 +17,5 @@ "jasmine", "node" ] - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "src/*/files/**/*" - ] + } } diff --git a/schematics/utils/html.ts b/schematics/utils/html.ts index f7b321f9aede..e08b8e41c9db 100644 --- a/schematics/utils/html.ts +++ b/schematics/utils/html.ts @@ -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}`); diff --git a/schematics/utils/lib-versions.ts b/schematics/utils/lib-versions.ts index 5046b92e16d7..458252a35594 100644 --- a/schematics/utils/lib-versions.ts +++ b/schematics/utils/lib-versions.ts @@ -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'; diff --git a/schematics/utils/testing.ts b/schematics/utils/testing.ts index 34b4378ef47e..10273f1126d3 100644 --- a/schematics/utils/testing.ts +++ b/schematics/utils/testing.ts @@ -18,8 +18,8 @@ export function createTestApp() { viewEncapsulation: 'None', version: '1.2.3', routing: true, - style: 'css', + style: 'scss', skipTests: false, minimal: false, }); -} \ No newline at end of file +}