From ddc3aea618dda64725027539b81d961ec52541d3 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Tue, 5 Feb 2019 14:25:20 -0500 Subject: [PATCH 1/5] feat(schematics): add custom component schematic --- collection.json | 4 +- package.json | 4 +- .../__name@dasherize__.component.__styleext__ | 0 .../__name@dasherize__.component.html | 3 + .../__name@dasherize__.component.spec.ts | 27 +++++ .../__name@dasherize__.component.ts | 14 +++ .../__name@dasherize__.module.ts | 14 +++ schematics/component/index.ts | 101 ++++++++++++++++++ schematics/component/schema.d.ts | 11 ++ schematics/component/schema.json | 69 ++++++++++++ 10 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ create mode 100644 schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html create mode 100644 schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts create mode 100644 schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts create mode 100644 schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts create mode 100644 schematics/component/index.ts create mode 100644 schematics/component/schema.d.ts create mode 100644 schematics/component/schema.json diff --git a/collection.json b/collection.json index 10be7df..b15aba4 100644 --- a/collection.json +++ b/collection.json @@ -12,7 +12,9 @@ }, "component": { "aliases": ["c"], - "extends": "@schematics/angular:component" + "factory": "./schematics/component", + "description": "Create an Angular component.", + "schema": "./schematics/component/schema.json" }, "directive": { "aliases": ["d"], diff --git a/package.json b/package.json index 785d6f9..9e9da52 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "devDependencies": { "@angular-devkit/architect": "^0.12.3", "@angular-devkit/build-angular": "^0.12.3", - "@angular-devkit/core": "^7.1.3", - "@angular-devkit/schematics": "^7.1.3", + "@angular-devkit/core": "~7.2.0", + "@angular-devkit/schematics": "~7.2.0", "@semantic-release/changelog": "^3.0.0", "@semantic-release/git": "^7.0.4", "@semantic-release/github": "^5.0.6", diff --git a/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ new file mode 100644 index 0000000..e69de29 diff --git a/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html new file mode 100644 index 0000000..0e75382 --- /dev/null +++ b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html @@ -0,0 +1,3 @@ +

+ <%= dasherize(name) %> works! +

diff --git a/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts new file mode 100644 index 0000000..4b40b77 --- /dev/null +++ b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts @@ -0,0 +1,27 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { <%= classify(name) %>Page } from './<%= dasherize(name) %>.page'; + +describe('<%= classify(name) %>Page', () => { + let component: <%= classify(name) %>Page; + let fixture: ComponentFixture<<%= classify(name) %>Page>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ <%= classify(name) %>Page ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(<%= classify(name) %>Page); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts new file mode 100644 index 0000000..440a22c --- /dev/null +++ b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: '<%= selector %>', + templateUrl: './<%= dasherize(name) %>.component.html', + styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>'], +}) +export class <%= classify(name) %>Component implements OnInit { + + constructor() { } + + ngOnInit() {} + +} diff --git a/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts new file mode 100644 index 0000000..0891698 --- /dev/null +++ b/schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { IonicModule } from '@ionic/angular'; + +import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component'; + +@NgModule({ + imports: [ CommonModule, FormsModule,IonicModule,], + declarations: [<%= classify(name) %>Component], + exports: [<%= classify(name) %>Component] +}) +export class <%= classify(name) %>ComponentModule {} diff --git a/schematics/component/index.ts b/schematics/component/index.ts new file mode 100644 index 0000000..5810930 --- /dev/null +++ b/schematics/component/index.ts @@ -0,0 +1,101 @@ +import { strings } from '@angular-devkit/core'; +import { Rule, SchematicsException, Tree, apply, branchAndMerge, chain, filter, mergeWith, move, noop, template, url } from '@angular-devkit/schematics'; +import { addSymbolToNgModuleMetadata } from '@schematics/angular/utility/ast-utils'; +import { InsertChange } from '@schematics/angular/utility/change'; +import { buildRelativePath } from '@schematics/angular/utility/find-module'; +import { parseName } from '@schematics/angular/utility/parse-name'; +import { buildDefaultPath, getProject } from '@schematics/angular/utility/project'; +import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; +import * as ts from 'typescript'; + +import { Schema as ComponentOptions } from './schema'; + +function buildSelector(options: ComponentOptions, projectPrefix: string) { + let selector = strings.dasherize(options.name); + + if (options.prefix) { + selector = `${options.prefix}-${selector}`; + } else if (options.prefix === undefined && projectPrefix) { + selector = `${projectPrefix}-${selector}`; + } + + return selector; +} + +function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile { + const text = host.read(modulePath); + if (text === null) { + throw new SchematicsException(`File ${modulePath} does not exist.`); + } + const sourceText = text.toString('utf-8'); + + return ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); +} + +function addImportToNgModule(options: ComponentOptions): Rule { + return (host: Tree) => { + if (!options.module) { + return host; + } + + const modulePath = options.module; + const moduleSource = readIntoSourceFile(host, modulePath); + + const componentModulePath = `/${options.path}/` + + (options.flat ? '' : strings.dasherize(options.name) + '/') + + strings.dasherize(options.name) + + '.module'; + + const relativePath = buildRelativePath(modulePath, componentModulePath); + const classifiedName = strings.classify(`${options.name}ComponentModule`); + const importChanges = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', classifiedName, relativePath); + + const importRecorder = host.beginUpdate(modulePath); + for (const change of importChanges) { + if (change instanceof InsertChange) { + importRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(importRecorder); + return host; + }; +} + +export default function(options: ComponentOptions): Rule { + return (host, context) => { + if (!options.project) { + throw new SchematicsException('Option (project) is required.'); + } + + const project = getProject(host, options.project); + + if (options.path === undefined) { + options.path = buildDefaultPath(project); + } + + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + options.selector = options.selector ? options.selector : buildSelector(options, project.prefix); + + validateName(options.name); + validateHtmlSelector(options.selector); + + const templateSource = apply(url('./files'), [ + options.spec ? noop() : filter(p => !p.endsWith('.spec.ts')), + template({ + ...strings, + 'if-flat': (s: string) => options.flat ? '' : s, + ...options, + }), + move(parsedPath.path), + ]); + + return chain([ + branchAndMerge(chain([ + addImportToNgModule(options), + mergeWith(templateSource), + ])), + ])(host, context); + }; +} diff --git a/schematics/component/schema.d.ts b/schematics/component/schema.d.ts new file mode 100644 index 0000000..9829448 --- /dev/null +++ b/schematics/component/schema.d.ts @@ -0,0 +1,11 @@ +export interface Schema { + path?: string; + project?: string; + name: string; + prefix?: string; + styleext?: string; + spec?: boolean; + flat?: boolean; + selector?: string; + module?: string; +} diff --git a/schematics/component/schema.json b/schematics/component/schema.json new file mode 100644 index 0000000..1a826e1 --- /dev/null +++ b/schematics/component/schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsIonicAngularComponent", + "title": "@ionic/angular Component Options Schema", + "type": "object", + "properties": { + "path": { + "type": "string", + "format": "path", + "description": "The path to create the page", + "visible": false + }, + "project": { + "type": "string", + "description": "The name of the project", + "$default": { + "$source": "projectName" + } + }, + "name": { + "type": "string", + "description": "The name of the page", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "prefix": { + "type": "string", + "description": "The prefix to apply to generated selectors", + "alias": "p", + "oneOf": [ + { + "maxLength": 0 + }, + { + "minLength": 1, + "format": "html-selector" + } + ] + }, + "styleext": { + "type": "string", + "description": "The file extension of the style file for the page", + "default": "css" + }, + "spec": { + "type": "boolean", + "description": "Specifies if a spec file is generated", + "default": true + }, + "flat": { + "type": "boolean", + "description": "Flag to indicate if a dir is created", + "default": false + }, + "selector": { + "type": "string", + "format": "html-selector", + "description": "The selector to use for the page" + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module", + "alias": "m" + } + }, + "required": [] +} From 2191d0846a917d8a2b3c6baa40207c89f9f92a67 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Tue, 5 Feb 2019 15:40:54 -0500 Subject: [PATCH 2/5] chore(): create utils --- package.json | 3 ++- schematics/component/index.ts | 14 ++------------ schematics/page/index.ts | 14 ++------------ schematics/util/index.ts | 13 +++++++++++++ tsconfig.json | 15 ++++----------- 5 files changed, 23 insertions(+), 36 deletions(-) create mode 100644 schematics/util/index.ts diff --git a/package.json b/package.json index 9e9da52..9f75b53 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "rimraf": "^2.6.2", "semantic-release": "^15.9.17", "tslint": "^5.12.0", - "tslint-ionic-rules": "0.0.21" + "tslint-ionic-rules": "0.0.21", + "typescript-tslint-plugin": "0.3.1" }, "peerDependencies": { "@angular-devkit/architect": ">=0.7.2", diff --git a/schematics/component/index.ts b/schematics/component/index.ts index 5810930..dde29a9 100644 --- a/schematics/component/index.ts +++ b/schematics/component/index.ts @@ -8,19 +8,9 @@ import { buildDefaultPath, getProject } from '@schematics/angular/utility/projec import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; import * as ts from 'typescript'; -import { Schema as ComponentOptions } from './schema'; - -function buildSelector(options: ComponentOptions, projectPrefix: string) { - let selector = strings.dasherize(options.name); - - if (options.prefix) { - selector = `${options.prefix}-${selector}`; - } else if (options.prefix === undefined && projectPrefix) { - selector = `${projectPrefix}-${selector}`; - } +import { buildSelector } from '../util'; - return selector; -} +import { Schema as ComponentOptions } from './schema'; function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile { const text = host.read(modulePath); diff --git a/schematics/page/index.ts b/schematics/page/index.ts index 9ce8ffb..6382fc4 100644 --- a/schematics/page/index.ts +++ b/schematics/page/index.ts @@ -8,6 +8,8 @@ import { buildDefaultPath, getProject } from '@schematics/angular/utility/projec import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; import * as ts from 'typescript'; +import { buildSelector } from '../util'; + import { Schema as PageOptions } from './schema'; function findRoutingModuleFromOptions(host: Tree, options: ModuleOptions): Path | undefined { @@ -137,18 +139,6 @@ function addRouteToRoutesArray(source: ts.SourceFile, ngModulePath: string, rout return []; } -function buildSelector(options: PageOptions, projectPrefix: string) { - let selector = strings.dasherize(options.name); - - if (options.prefix) { - selector = `${options.prefix}-${selector}`; - } else if (options.prefix === undefined && projectPrefix) { - selector = `${projectPrefix}-${selector}`; - } - - return selector; -} - export default function(options: PageOptions): Rule { return (host, context) => { if (!options.project) { diff --git a/schematics/util/index.ts b/schematics/util/index.ts new file mode 100644 index 0000000..f3c3be8 --- /dev/null +++ b/schematics/util/index.ts @@ -0,0 +1,13 @@ +import { strings } from '@angular-devkit/architect/node_modules/@angular-devkit/core'; + +export function buildSelector(options: any, projectPrefix: string) { + let selector = strings.dasherize(options.name); + + if (options.prefix) { + selector = `${options.prefix}-${selector}`; + } else if (options.prefix === undefined && projectPrefix) { + selector = `${projectPrefix}-${selector}`; + } + + return selector; +} diff --git a/tsconfig.json b/tsconfig.json index 1f3002c..9f1a9cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,16 +10,9 @@ "pretty": true, "strict": true, "target": "es2017", - "lib": [ - "es2017" - ] + "lib": ["es2017"], + "plugins": [{ "name": "typescript-tslint-plugin" }] }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "node_modules", - "**/__tests__/*.ts", - "schematics/*/files" - ] + "include": ["**/*.ts"], + "exclude": ["node_modules", "**/__tests__/*.ts", "schematics/*/files"] } From d421b2f3f4a32e48cfd0ff07d2bd5e8c779da8fd Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Tue, 5 Feb 2019 16:50:08 -0500 Subject: [PATCH 3/5] chore(): fix import path --- schematics/util/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schematics/util/index.ts b/schematics/util/index.ts index f3c3be8..3651a2d 100644 --- a/schematics/util/index.ts +++ b/schematics/util/index.ts @@ -1,4 +1,4 @@ -import { strings } from '@angular-devkit/architect/node_modules/@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; export function buildSelector(options: any, projectPrefix: string) { let selector = strings.dasherize(options.name); From 86c9e03cc39f8d71888c626d28b2c63330cc1db2 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Wed, 6 Feb 2019 11:56:13 -0500 Subject: [PATCH 4/5] feat(schematics): customize component creation --- schematics/component/index.ts | 78 ++++++++++++++++++++++++++++++-- schematics/component/schema.d.ts | 3 ++ schematics/component/schema.json | 20 ++++++-- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/schematics/component/index.ts b/schematics/component/index.ts index dde29a9..a14021d 100644 --- a/schematics/component/index.ts +++ b/schematics/component/index.ts @@ -1,6 +1,6 @@ import { strings } from '@angular-devkit/core'; import { Rule, SchematicsException, Tree, apply, branchAndMerge, chain, filter, mergeWith, move, noop, template, url } from '@angular-devkit/schematics'; -import { addSymbolToNgModuleMetadata } from '@schematics/angular/utility/ast-utils'; +import { addDeclarationToModule, addEntryComponentToModule, addExportToModule, addSymbolToNgModuleMetadata } from '@schematics/angular/utility/ast-utils'; import { InsertChange } from '@schematics/angular/utility/change'; import { buildRelativePath } from '@schematics/angular/utility/find-module'; import { parseName } from '@schematics/angular/utility/parse-name'; @@ -27,7 +27,79 @@ function addImportToNgModule(options: ComponentOptions): Rule { if (!options.module) { return host; } + if (!options.createModule && options.module) { + addImportToDeclarations(host, options); + } + if (options.createModule && options.module) { + addImportToImports(host, options); + } + return host; + }; +} + +function addImportToDeclarations(host: Tree, options: ComponentOptions): void { + if (options.module) { + const modulePath = options.module; + let source = readIntoSourceFile(host, modulePath); + + const componentPath = `/${options.path}/` + + (options.flat ? '' : strings.dasherize(options.name) + '/') + + strings.dasherize(options.name) + + '.component'; + const relativePath = buildRelativePath(modulePath, componentPath); + const classifiedName = strings.classify(`${options.name}Component`); + const declarationChanges = addDeclarationToModule(source, + modulePath, + classifiedName, + relativePath); + + const declarationRecorder = host.beginUpdate(modulePath); + for (const change of declarationChanges) { + if (change instanceof InsertChange) { + declarationRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(declarationRecorder); + + if (options.export) { + // Need to refresh the AST because we overwrote the file in the host. + source = readIntoSourceFile(host, modulePath); + const exportRecorder = host.beginUpdate(modulePath); + const exportChanges = addExportToModule(source, modulePath, + strings.classify(`${options.name}Component`), + relativePath); + + for (const change of exportChanges) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(exportRecorder); + } + + if (options.entryComponent) { + // Need to refresh the AST because we overwrote the file in the host. + source = readIntoSourceFile(host, modulePath); + + const entryComponentRecorder = host.beginUpdate(modulePath); + const entryComponentChanges = addEntryComponentToModule( + source, modulePath, + strings.classify(`${options.name}Component`), + relativePath); + + for (const change of entryComponentChanges) { + if (change instanceof InsertChange) { + entryComponentRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(entryComponentRecorder); + } + } +} + +function addImportToImports(host: Tree, options: ComponentOptions): void { + if (options.module) { const modulePath = options.module; const moduleSource = readIntoSourceFile(host, modulePath); @@ -47,8 +119,7 @@ function addImportToNgModule(options: ComponentOptions): Rule { } } host.commitUpdate(importRecorder); - return host; - }; + } } export default function(options: ComponentOptions): Rule { @@ -73,6 +144,7 @@ export default function(options: ComponentOptions): Rule { const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(p => !p.endsWith('.spec.ts')), + options.createModule ? noop() : filter(p => !p.endsWith('.module.ts')), template({ ...strings, 'if-flat': (s: string) => options.flat ? '' : s, diff --git a/schematics/component/schema.d.ts b/schematics/component/schema.d.ts index 9829448..e022dbf 100644 --- a/schematics/component/schema.d.ts +++ b/schematics/component/schema.d.ts @@ -7,5 +7,8 @@ export interface Schema { spec?: boolean; flat?: boolean; selector?: string; + createModule?: boolean; module?: string; + export?: boolean; + entryComponent?: boolean; } diff --git a/schematics/component/schema.json b/schematics/component/schema.json index 1a826e1..ba399b2 100644 --- a/schematics/component/schema.json +++ b/schematics/component/schema.json @@ -59,10 +59,24 @@ "format": "html-selector", "description": "The selector to use for the page" }, - "module": { + "createModule": { + "type": "boolean", + "description": "Allows creating a module for the component", + "default": false + }, + "module": { "type": "string", - "description": "Allows specification of the declaring module", - "alias": "m" + "description": "Allows adding to a modules imports or declarations" + }, + "export": { + "type": "boolean", + "default": false, + "description": "When true, the declaring NgModule exports this component." + }, + "entryComponent": { + "type": "boolean", + "default": false, + "description": "When true, the new component is the entry component of the declaring NgModule." } }, "required": [] From a5c13c027475e427075efa8b3c69848181b72f52 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Wed, 6 Feb 2019 12:12:40 -0500 Subject: [PATCH 5/5] chore(): spelling --- schematics/component/schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schematics/component/schema.json b/schematics/component/schema.json index ba399b2..b1b14f5 100644 --- a/schematics/component/schema.json +++ b/schematics/component/schema.json @@ -61,12 +61,12 @@ }, "createModule": { "type": "boolean", - "description": "Allows creating a module for the component", + "description": "Allows creating an NgModule for the component", "default": false }, "module": { "type": "string", - "description": "Allows adding to a modules imports or declarations" + "description": "Allows adding to an NgModule's imports or declarations" }, "export": { "type": "boolean",