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..9f75b53 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", @@ -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/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..a14021d --- /dev/null +++ b/schematics/component/index.ts @@ -0,0 +1,163 @@ +import { strings } from '@angular-devkit/core'; +import { Rule, SchematicsException, Tree, apply, branchAndMerge, chain, filter, mergeWith, move, noop, template, url } from '@angular-devkit/schematics'; +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'; +import { buildDefaultPath, getProject } from '@schematics/angular/utility/project'; +import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; +import * as ts from 'typescript'; + +import { buildSelector } from '../util'; + +import { Schema as ComponentOptions } from './schema'; + +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; + } + 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); + + 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); + } +} + +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')), + options.createModule ? noop() : filter(p => !p.endsWith('.module.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..e022dbf --- /dev/null +++ b/schematics/component/schema.d.ts @@ -0,0 +1,14 @@ +export interface Schema { + path?: string; + project?: string; + name: string; + prefix?: string; + styleext?: string; + 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 new file mode 100644 index 0000000..b1b14f5 --- /dev/null +++ b/schematics/component/schema.json @@ -0,0 +1,83 @@ +{ + "$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" + }, + "createModule": { + "type": "boolean", + "description": "Allows creating an NgModule for the component", + "default": false + }, + "module": { + "type": "string", + "description": "Allows adding to an NgModule's 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": [] +} 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..3651a2d --- /dev/null +++ b/schematics/util/index.ts @@ -0,0 +1,13 @@ +import { strings } from '@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"] }