/* Copyright IBM Corp. 2017 */ import * as escapeStringRegexp from 'escape-string-regexp'; import { createReadStream } from 'graceful-fs'; import { cloneDeep, forEach, noop, replace, upperFirst } from 'lodash'; import { join, normalize, parse, relative } from 'path'; import { combineLatest, concat, defer, empty, from, merge, Observable, Observer, of, Subject } from 'rxjs'; import { catchError, distinct, filter, map, mapTo, mergeMap, mergeMapTo, share, shareReplay, skipWhile, startWith, tap, toArray } from 'rxjs/operators'; import { ComponentClass, ComponentRegistry } from '../utils/component.reg'; import { arrayOf, firstOf } from '../utils/file.utils'; import { LayoutClass, LayoutRegistry } from '../utils/layout.reg'; import { rxMkDirs } from '../utils/rx.copy'; import { rxForkJoin } from '../utils/rx.utils'; import { TypeClass, TypeRegistry } from '../utils/type.reg'; import { APP_MODULE_FILE, CMD_CREATE, LAYOUTS_EXPORT_FILE, LAYOUTS_FILE, MODULES_EXPORT_FILE, MODULES_FILE, OPTION_DATA, OPTION_EDITABLE, OPTION_FLAT, OPTION_MODULE, OPTION_MODULES, OPTION_SCSS, OPTION_SOURCE, OPTION_VERBOSE } from './../constants'; import { Options, optNormalizeDir } from './../options'; import { relativePath } from './../utils/file.utils'; import { canonicalizeJSON } from './../utils/json'; import { writeTextFileSafe } from './../utils/rx.file'; import { hbsCompile } from './../utils/rx.hbs'; import { rxReadLines } from './../utils/rx.lines'; import { rxTidy } from './../utils/rx.tidy'; import { FileDescriptor, rxStats, walkFiles } from './../utils/rx.walk'; import { camelCase, kebabCase } from './../utils/transliteration'; import { addLayoutToModule } from './../utils/ts.add.layout'; import { Matcher, WchTools, wchToolsGetMatcher } from './../utils/wchtools'; import { AbstractCommand } from './abstract.command'; import { Command } from './command'; import { HELP_COMPONENT_SOURCE, HELP_DATA_DIRECTORY, HELP_EDITABLE, HELP_FLAT, HELP_MODULE_FILE, HELP_MODULES, HELP_SCSS, HELP_VERBOSE } from './help'; const ASSETS_BASE = join(__dirname, '..', '..', '..', 'assets'); const TYPE_GROUP = 'Group'; const TYPE_BINDING = 'RenderingContextBinding'; const TYPE_CONTEXT = 'RenderingContext'; const TYPE_ABSTRACT_COMPONENT = 'AbstractRenderingComponent'; const DEP_SDK = '@ibm-wch-sdk/ng'; const DEP_UTILS = '@ibm-wch-sdk/utils'; declare type TypeMatcher = (aType: any) => boolean; function _camelCase(aValue: string): string { return camelCase(aValue); } function _plural(aKey: string) { return aKey + 's'; } function _toComment(aValue: any): string { return JSON.stringify(canonicalizeJSON(aValue), undefined, 2) .split('\n') .map(line => ' * ' + line) .join('\n'); } const SINGLE_ELEMENT_TYPES: { [key: string]: string } = {}; SINGLE_ELEMENT_TYPES['text'] = 'SingleTextElement'; SINGLE_ELEMENT_TYPES['number'] = 'SingleNumberElement'; SINGLE_ELEMENT_TYPES['toggle'] = 'SingleToggleElement'; SINGLE_ELEMENT_TYPES['formattedtext'] = 'SingleFormattedTextElement'; SINGLE_ELEMENT_TYPES['link'] = 'SingleLinkElement'; SINGLE_ELEMENT_TYPES['datetime'] = 'SingleDateElement'; SINGLE_ELEMENT_TYPES['file'] = 'SingleFileElement'; SINGLE_ELEMENT_TYPES['video'] = 'SingleVideoElement'; SINGLE_ELEMENT_TYPES['image'] = 'SingleImageElement'; SINGLE_ELEMENT_TYPES['optionselection'] = 'SingleOptionSelectionElement'; SINGLE_ELEMENT_TYPES['reference'] = 'SingleReferenceElement'; SINGLE_ELEMENT_TYPES['category'] = 'CategoryElement'; SINGLE_ELEMENT_TYPES['location'] = 'LocationElement'; function _getElementValidator(aType: string): string { const type = SINGLE_ELEMENT_TYPES[aType]; return ( 'is' + (type && type.startsWith('Single') ? type.substr('Single'.length) : type) ); } const MULTI_ELEMENT_TYPES: { [key: string]: string } = {}; MULTI_ELEMENT_TYPES['text'] = 'MultiTextElement'; MULTI_ELEMENT_TYPES['number'] = 'MultiNumberElement'; MULTI_ELEMENT_TYPES['toggle'] = 'MultiToggleElement'; MULTI_ELEMENT_TYPES['formattedtext'] = 'MultiFormattedTextElement'; MULTI_ELEMENT_TYPES['link'] = 'MultiLinkElement'; MULTI_ELEMENT_TYPES['datetime'] = 'MultiDateElement'; MULTI_ELEMENT_TYPES['file'] = 'MultiFileElement'; MULTI_ELEMENT_TYPES['video'] = 'MultiVideoElement'; MULTI_ELEMENT_TYPES['image'] = 'MultiImageElement'; MULTI_ELEMENT_TYPES['optionselection'] = 'MultiOptionSelectionElement'; MULTI_ELEMENT_TYPES['reference'] = 'MultiReferenceElement'; const TYPES: { [key: string]: string } = {}; TYPES['text'] = 'string'; TYPES['number'] = 'number'; TYPES['toggle'] = 'boolean'; TYPES['formattedtext'] = 'string'; TYPES['link'] = 'Link'; TYPES['datetime'] = 'Date'; TYPES['file'] = 'File'; TYPES['video'] = 'Video'; TYPES['image'] = 'Image'; TYPES['optionselection'] = 'OptionSelection'; TYPES['reference'] = 'RenderingContext'; TYPES['category'] = 'Category'; TYPES['location'] = 'Location'; const AUGMENTED_TYPES: { [key: string]: string } = {}; AUGMENTED_TYPES['Image'] = 'SingleImageElement'; AUGMENTED_TYPES['Video'] = 'SingleVideoElement'; AUGMENTED_TYPES['File'] = 'SingleFileElement'; AUGMENTED_TYPES['Link'] = 'SingleLinkElement'; AUGMENTED_TYPES['Category'] = 'CategoryElement'; AUGMENTED_TYPES['Location'] = 'LocationElement'; const DEFAULT_VALUES: { [key: string]: string } = {}; DEFAULT_VALUES['text'] = "''"; DEFAULT_VALUES['number'] = '0'; DEFAULT_VALUES['toggle'] = 'false'; DEFAULT_VALUES['formattedtext'] = "''"; const BUILT_IN_TYPES = ['string', 'number', 'boolean', 'Date']; export interface Templates { abstractComponent: Observable; typeComponent: Observable; typeInterface: Observable; typeDefinition: Observable; typeHtml: Observable; typeCss: Observable; layoutModule: Observable; layoutComponent: Observable; layoutHtml: Observable; layoutCss: Observable; layoutOverview: Observable; layoutExports: Observable; moduleOverview: Observable; moduleExports: Observable; } function _createTemplate( aOptions: Options, aLogger: Subject ): Templates { const rxBaseComponent = hbsCompile(join(ASSETS_BASE, 'component.hbs')); const rxTypeComponent = hbsCompile(join(ASSETS_BASE, 'type.component.hbs')); const rxTypeInterface = hbsCompile(join(ASSETS_BASE, 'type.interface.hbs')); const rxTypeDefinition = hbsCompile(join(ASSETS_BASE, 'type.definition.hbs')); const rxTypeHtml = hbsCompile(join(ASSETS_BASE, 'type.component.html.hbs')); const rxTypeCss = hbsCompile(join(ASSETS_BASE, 'type.component.css.hbs')); const rxLayoutComponent = hbsCompile( join(ASSETS_BASE, 'layout.component.hbs') ); const rxLayoutModule = hbsCompile(join(ASSETS_BASE, 'layout.module.hbs')); // allow to override the template const layoutHtmlFile = aOptions.layoutHtmlTemplate ? optNormalizeDir(aOptions.layoutHtmlTemplate) : join(ASSETS_BASE, 'layout.component.html.hbs'); // log this aLogger.next(`LayoutHtmlTemplate[$layoutHtmlFile}].`); const rxLayoutHtml = hbsCompile(layoutHtmlFile); const rxLayoutCss = hbsCompile(join(ASSETS_BASE, 'layout.component.css.hbs')); const rxLayoutOverview = hbsCompile(join(ASSETS_BASE, 'layout.overview.hbs')); const rxLayoutExports = hbsCompile(join(ASSETS_BASE, 'layout.export.hbs')); const rxModuleOverview = hbsCompile(join(ASSETS_BASE, 'module.overview.hbs')); const rxModuleExports = hbsCompile(join(ASSETS_BASE, 'module.export.hbs')); // return return { abstractComponent: rxBaseComponent, typeComponent: rxTypeComponent, typeInterface: rxTypeInterface, typeDefinition: rxTypeDefinition, typeHtml: rxTypeHtml, typeCss: rxTypeCss, layoutComponent: rxLayoutComponent, layoutModule: rxLayoutModule, layoutHtml: rxLayoutHtml, layoutCss: rxLayoutCss, layoutOverview: rxLayoutOverview, layoutExports: rxLayoutExports, moduleOverview: rxModuleOverview, moduleExports: rxModuleExports }; } function _getExpression(aElement: any) { const type = aElement.allowMultipleValues ? _plural(aElement.elementType) : aElement.elementType; return type + '.' + aElement.key; } function _createTypeComponentContext( aClass: ComponentClass, aTools: WchTools ): any { // clone the type const cloned = cloneDeep(aClass.typeDef.type); const types: { [key: string]: string } = {}; // set class name cloned.baseClassName = aClass.abstractComponentClass; cloned.className = aClass.typeComponentClass; cloned.baseFileName = './' + aClass.abstractComponentRef; cloned.templateUrl = './' + aClass.typeTemplateFile; cloned.styleUrl = './' + aClass.typeStyleFile; cloned.selector = aClass.typeSelector; // the imports cloned.imports = Object.keys(types).sort(); return cloned; } function _createLayoutComponentContext( aClass: LayoutClass, aTools: WchTools, aOptions: Options ): any { const cloned = cloneDeep(aClass.layout); // check the editable flag cloned.editable = aOptions.editable || false; // classname cloned.className = aClass.layoutClass; cloned.baseClassName = aClass.component.typeComponentClass; cloned.selector = aClass.layoutSelector; cloned.layoutTemplate = aClass.layoutTemplate; cloned.templateUrl = './' + aClass.layoutTemplateFile; cloned.styleUrl = './' + aClass.layoutStyleFile; // path to the layout const layoutPath = aClass.folder; const componentPath = aClass.component.folder; // compute the relative path const relPath = relativePath(layoutPath, componentPath); cloned.baseFileName = './' + relPath + '/' + aClass.component.typeComponentRef; return cloned; } function _createLayoutModuleContext( aClass: LayoutClass, aTools: WchTools, aOptions: Options ): any { // dispatch const ctx = _createLayoutComponentContext(aClass, aTools, aOptions); // augment the context const moduleClassName = aClass.moduleClass; ctx.moduleClassName = moduleClassName; ctx.layoutClassName = aClass.layoutClass; // path to the layout and module const layoutPath = aClass.folder; const modulePath = aClass.folder; // remember ctx.layoutPath = layoutPath; ctx.layoutModulePath = modulePath; // compute the relative path ctx.layoutFileName = './' + aClass.layoutRef; return ctx; } function _createAbstractComponentContext( aClass: ComponentClass, aComponentReg: ComponentRegistry, aTools: WchTools ): Observable { // clone the type const cloned = cloneDeep(aClass.typeDef.type); // set class name cloned.className = aClass.abstractComponentClass; // set class name cloned.interfacesName = aClass.typeInterfaceClass; // target name cloned.interfacesFileName = aClass.typeInterfaceRef; // path to the type interface const interfacePath = aClass.folder; // type checks const typeChecks = (cloned.typeChecks = {}); // produce the bindings const imports: Imports = {}; // always add rendering context _registerImport(TYPE_CONTEXT, DEP_SDK, imports); _registerImport(TYPE_ABSTRACT_COMPONENT, DEP_SDK, imports); // register the type checks const interfaceImport = `./${aClass.typeInterfaceRef}`; _registerImport(cloned.interfacesName, interfaceImport, imports); _registerImport(`is${cloned.interfacesName}`, interfaceImport, imports); _registerImport(`assert${cloned.interfacesName}`, interfaceImport, imports); // check if we have bindings if (cloned.elements && cloned.elements.length > 0) { // also add bindings _registerImport(TYPE_BINDING, DEP_SDK, imports); } // work on the bindings const rxBindings: Observable[] = cloned.elements.map((el: any) => { // result const binding: any = {}; // class name binding.elKey = el.key; binding.key = _camelCase(el.key); binding.observableKey = _camelCase('on ' + el.key); binding.expression = _getExpression(el); binding.comment = _toComment(el); binding.required = !!el.required; binding.optional = !!!el.required; // defaults if (!el.required) { if (el.allowMultipleValues) { binding.default = '[]'; } else { binding.default = DEFAULT_VALUES[el.elementType]; } } // type let rxTypeName: Observable; // check for the element type const bIsGroup = el.elementType === 'group'; if (bIsGroup) { // type id const typeId = el.typeRef.id; // locate the type const rxRefType = aComponentReg .findTypeClass(aTools.types[typeId]) .pipe(shareReplay()); // the from statement const rxFrom = rxRefType.pipe( map( typeDef => './' + relativePath(interfacePath, typeDef.folder) + '/' + typeDef.typeElementRef ) ); // the type name const rxType = rxRefType.pipe(map(typeDef => typeDef.simpleTypeClass)); // register the import const rxImportType = combineLatest(rxFrom, rxType).pipe( map(([from, name]) => _registerImport(name, from, imports)) ); // the type name rxTypeName = rxImportType.pipe(mergeMapTo(rxType)); } else { // type const type = TYPES[el.elementType]; if (BUILT_IN_TYPES.indexOf(type) < 0) { _registerImport(type, DEP_SDK, imports); } // get the type rxTypeName = of(type); } return rxTypeName.pipe( map(typeName => { // update the type field binding.type = el.allowMultipleValues ? typeName + '[]' : typeName; // add return binding; }) ); }); // wait return rxForkJoin(rxBindings).pipe( map(bindings => { // update cloned.bindings = bindings; // organize const otherImports: { [key: string]: string[] } = {}; Object.keys(imports) .sort() .forEach( from => (otherImports[from] = Object.keys(imports[from]).sort()) ); // update cloned.otherImports = otherImports; // add the types // ok return cloned; }) ); } function _createConstantName(aKey: string): string { return replace(kebabCase(aKey).toUpperCase(), /\-/g, '_'); } interface ImportedClasses { [key: string]: string; } interface Imports { [from: string]: ImportedClasses; } function _registerImport(aClass: string, aFrom: string, aImports: Imports) { // create the hook const imp: ImportedClasses = aImports[aFrom] || (aImports[aFrom] = {}); // add imp[aClass] = aClass; } function _createTypeInterfaceContext( aClass: ComponentClass, aComponentReg: ComponentRegistry, aTools: WchTools ): Observable { // clone the type const typeDef = aClass.typeDef; const cloned = cloneDeep(aClass.typeDef.type); // set class name cloned.interfacesName = aClass.typeInterfaceClass; cloned.definitionName = aClass.typeDef.typeElementClass; // path to the type interface const interfacePath = aClass.folder; const definitionPath = aClass.typeDef.folder; // compute the relative path const relPath = relativePath(interfacePath, definitionPath); const definitionRef = './' + relPath + '/' + aClass.typeDef.typeElementRef; // top level fields const expressions: any = {}; // produce the bindings const imports: Imports = {}; // the well known type _registerImport(cloned.definitionName, definitionRef, imports); _registerImport(`is${cloned.definitionName}`, definitionRef, imports); const rxBindings: Observable[] = cloned.elements.map((el: any) => { // result const binding: any = {}; // class name binding.constant = 'KEY_' + _createConstantName(el.key); binding.key = el.key; binding.observableKey = _camelCase('on ' + el.key); binding.expression = _getExpression(el); binding.comment = _toComment(el); binding.required = !!el.required; binding.optional = !!!el.required; // import the constant _registerImport(binding.constant, definitionRef, imports); let rxTypeName: Observable; const bIsGroup = el.elementType === 'group'; if (bIsGroup) { // type id const typeId = el.typeRef.id; // locate the type const rxRefType = aComponentReg .findTypeClass(aTools.types[typeId]) .pipe(shareReplay()); // the from statement const rxFrom = rxRefType.pipe( map( typeDef => './' + relativePath(interfacePath, typeDef.folder) + '/' + typeDef.typeElementRef ) ); // the type name const rxType = rxRefType.pipe(map(typeDef => typeDef.simpleTypeClass)); // register the import const rxImportType = combineLatest(rxFrom, rxType).pipe( map(([from, name]) => _registerImport(name, from, imports)) ); // the type name rxTypeName = rxImportType.pipe(mergeMapTo(rxType)); } else { const directType = TYPES[el.elementType]; const augType = AUGMENTED_TYPES[directType]; const expElType = augType || directType; if (BUILT_IN_TYPES.indexOf(expElType) < 0) { _registerImport(expElType, DEP_SDK, imports); } rxTypeName = of(expElType); } // get the top level entry const expType: string = el.allowMultipleValues ? _plural(el.elementType) : el.elementType; const exp: any = expressions[expType] || (expressions[expType] = {}); // handle return rxTypeName.pipe( map(typeName => { // update the expression exp[el.key] = { key: el.key, comment: _toComment(el), type: el.allowMultipleValues ? typeName + '[]' : typeName, optional: !!!el.required }; // returns the binding return binding; }) ); }); // wait return rxForkJoin(rxBindings).pipe( map(bindings => { // update cloned.bindings = bindings; // handle the imports const fromNg = imports[DEP_SDK] || {}; delete fromNg[TYPE_CONTEXT]; // the imports cloned.imports = Object.keys(fromNg).sort(); cloned.expressions = expressions; // the other importa delete imports[DEP_SDK]; // organize const otherImports: { [key: string]: string[] } = {}; Object.keys(imports) .sort() .forEach( from => (otherImports[from] = Object.keys(imports[from]).sort()) ); // update cloned.otherImports = otherImports; // ok return cloned; }) ); } function _createTypeDefinitionContext( aTypeClass: TypeClass, aComponentReg: ComponentRegistry, aTools: WchTools ): Observable { // clone the type const typeDef = aTypeClass; const cloned = cloneDeep(typeDef.type); // set class name cloned.definitionName = typeDef.typeElementClass; cloned.simpleTypeName = typeDef.simpleTypeClass; // path to the type interface const definitionPath = typeDef.folder; // produce the bindings const imports: Imports = {}; // add default import _registerImport('GroupElement', DEP_SDK, imports); _registerImport('isSingleGroupElement', DEP_SDK, imports); _registerImport('isMultiGroupElement', DEP_SDK, imports); const rxBindings: Observable[] = cloned.elements.map((el: any) => { // result const binding: any = {}; // class name binding.key = el.key; binding.constant = 'KEY_' + _createConstantName(el.key); binding.observableKey = _camelCase('on ' + el.key); binding.expression = _getExpression(el); binding.comment = _toComment(el); binding.required = !!el.required; binding.optional = !!!el.required; let rxElementName: Observable; let rxSimpleName: Observable; const bIsGroup = el.elementType === 'group'; if (bIsGroup) { // type id const typeId = el.typeRef.id; // locate the type const rxRefType = aComponentReg .findTypeClass(aTools.types[typeId]) .pipe(shareReplay()); // the from statement const rxFrom = rxRefType.pipe( map( typeDef => './' + relativePath(definitionPath, typeDef.folder) + '/' + typeDef.typeElementRef ) ); // the type name const rxNameEl = rxRefType.pipe( map( typeDef => el.allowMultipleValues ? typeDef.multiElementClass : typeDef.singleElementClass ), shareReplay() ); // register the import const rxImportElement = combineLatest(rxFrom, rxNameEl, rxRefType).pipe( map(([from, name, typeDef]) => { // test for idempotent imports if (definitionPath !== typeDef.folder) { _registerImport(name, from, imports); _registerImport(`is${name}`, from, imports); } }) ); // the type name rxElementName = rxImportElement.pipe(mergeMapTo(rxNameEl)); // the type name const rxNameSimp = rxRefType.pipe( map(typeDef => typeDef.simpleTypeClass), shareReplay() ); // register the import const rxImportSimple = combineLatest(rxFrom, rxNameSimp, rxRefType).pipe( map(([from, name, typeDef]) => { // test for idempotent imports if (definitionPath !== typeDef.folder) { _registerImport(name, from, imports); } }) ); // the type name rxSimpleName = rxImportSimple.pipe(mergeMapTo(rxNameSimp)); } else { // decode the type name const elementName = el.allowMultipleValues ? MULTI_ELEMENT_TYPES[el.elementType] : SINGLE_ELEMENT_TYPES[el.elementType]; if (BUILT_IN_TYPES.indexOf(elementName) < 0) { // register the import _registerImport(elementName, DEP_SDK, imports); // import the type check _registerImport(`is${elementName}`, DEP_SDK, imports); } // decode the type name const simpleName = TYPES[el.elementType]; if (BUILT_IN_TYPES.indexOf(simpleName) < 0) { // register the import _registerImport(simpleName, DEP_SDK, imports); } // ok rxSimpleName = of(simpleName); // ok rxElementName = of(elementName); } // handle return rxForkJoin([rxSimpleName, rxElementName]).pipe( map(([simpName, elName]) => { // update the binding binding.type = elName; binding.simpleTypeName = simpName; binding.validator = `is${elName}`; // returns the binding return binding; }) ); }); // wait return rxForkJoin(rxBindings).pipe( map(bindings => { // update cloned.bindings = bindings; cloned.hasBindings = Object.keys(bindings).length > 0; // organize const otherImports: { [key: string]: string[] } = {}; Object.keys(imports) .sort() .forEach( from => (otherImports[from] = Object.keys(imports[from]).sort()) ); // update cloned.otherImports = otherImports; cloned.baseElementClass = typeDef.baseElementClass; cloned.singleElementClass = typeDef.singleElementClass; cloned.multiElementClass = typeDef.multiElementClass; // ok return cloned; }) ); } function _createAbstractComponent( aClass: ComponentClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aComponentReg: ComponentRegistry, aLogger: Subject ): Observable { // prepare the context const rxCtx = _createAbstractComponentContext(aClass, aComponentReg, aTools); // target name const dstFile = join(aClass.folder, aClass.abstractComponentFile); // log this aLogger.next( `abstractComponent for ${aClass.typeDef.type.name} => ${dstFile}` ); // execute the template const newContent = rxCtx.pipe( mergeMap(ctx => aTemplate.pipe(map(tmp => tmp(ctx)))) ); // write the content return newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, true)) ); } function _createTypeInterface( aClass: ComponentClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aComponentReg: ComponentRegistry, aLogger: Subject ): Observable { // prepare the context const rxCtx = _createTypeInterfaceContext(aClass, aComponentReg, aTools); // target name const dstFile = join(aClass.folder, aClass.typeInterfaceFile); // log this aLogger.next(`typeInterface for ${aClass.typeDef.type.name} => ${dstFile}`); // execute the template const newContent = rxCtx.pipe( mergeMap(ctx => aTemplate.pipe(map(tmp => tmp(ctx)))) ); // write the content return newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, true)) ); } /** * Get the type dependencies * * @param aTypeClass */ function _getTypeDependencies( aTypeClass: TypeClass, aTools: WchTools, aComponentReg: ComponentRegistry ): Observable { // type const type = aTypeClass.type; // analyze the elements return merge( ...type.elements .filter((el: any) => el.elementType === 'group') .map((el: any) => aComponentReg.findTypeClass(aTools.types[el.typeRef.id]) ) ); } function _createTypeDefinition( aTypeClass: TypeClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aComponentReg: ComponentRegistry, aLogger: Subject ): Observable { // check for dups if (!aComponentReg.registerType(aTypeClass.type)) { return empty(); } // the def const typeDef = aTypeClass; // prepare the context const rxCtx = _createTypeDefinitionContext(typeDef, aComponentReg, aTools); // target name const dstFile = join(typeDef.folder, typeDef.typeElementFile); // log this aLogger.next(`typeDefinition for ${typeDef.type.name} => ${dstFile}`); // execute the template const newContent = rxCtx.pipe( mergeMap(ctx => aTemplate.pipe(map(tmp => tmp(ctx)))) ); // write the content const rxResult = newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, true)) ); // generate all transitive types const rxDependencies = _getTypeDependencies( typeDef, aTools, aComponentReg ).pipe( mergeMap(aDepType => _createTypeDefinition( aDepType, aTools, aTemplate, aOptions, aComponentReg, aLogger ) ) ); // merge together return merge(rxResult, rxDependencies); } function _createTypeComponent( aClass: ComponentClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aLogger: Subject ): Observable { // override flag const bOverride = !!aOptions.override; // prepare the context const ctx = _createTypeComponentContext(aClass, aTools); // target name const dstFile = join(aClass.folder, aClass.typeComponentFile); // log this aLogger.next(`typeComponent for ${aClass.typeDef.type.name} => ${dstFile}`); // execute the template const newContent = aTemplate.pipe(map(tmp => tmp(ctx))); // write the content return newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, bOverride)) ); } function _createTypeHtmlContext(aClass: ComponentClass, aTools: WchTools) { return { className: aClass.typeComponentClass }; } /** * Constructs the context to generate a layout sample * * @param aLayout the layout * @param aMapping the layout mapping * @param aType the type * @param aTools access to the tools */ function _createLayoutHtmlContext( aClass: LayoutClass, aTools: WchTools, aOptions: Options ) { // elements const el = cloneDeep(aClass.type.elements); // iterate and add the propery name forEach(el, (e: any) => { e.prop = _camelCase(e.key); e.onprop = _camelCase('on ' + e.key); }); return { className: aClass.layoutClass, editable: !!aOptions.editable || false, elements: el }; } function _createTypeHtml( aClass: ComponentClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aLogger: Subject ): Observable { // override flag const bOverride = !!aOptions.override; // prepare the context const ctx = _createTypeHtmlContext(aClass, aTools); // target name const dstFile = join(aClass.folder, aClass.typeTemplateFile); // log this aLogger.next(`typeTemplate for ${aClass.typeDef.type.name} => ${dstFile}`); // execute the template const newContent = aTemplate.pipe(map(tmp => tmp(ctx))); // write the content return newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, bOverride)) ); } function _createTypeCssContext(aClass: ComponentClass, aTools: WchTools) { return { className: aClass.typeComponentClass }; } function _createLayoutCssContext(aClass: LayoutClass, aTools: WchTools) { return { className: aClass.layoutClass }; } function _createTypeCss( aClass: ComponentClass, aTools: WchTools, aTemplate: Observable, aOptions: Options, aLogger: Subject ): Observable { // override flag const bOverride = !!aOptions.override; // prepare the context const ctx = _createTypeCssContext(aClass, aTools); // target name const dstFile = join(aClass.folder, aClass.typeStyleFile); // log this aLogger.next(`typeStyles for ${aClass.typeDef.type.name} => ${dstFile}`); // execute the template const newContent = aTemplate.pipe(map(tmp => tmp(ctx))); // write the content return newContent.pipe( mergeMap(data => writeTextFileSafe(dstFile, data, bOverride)) ); } function _createComponent( aClass: ComponentClass, aTools: WchTools, aTemplate: Templates, aOptions: Options, aComponentReg: ComponentRegistry, aLogger: Subject ): Observable { // log this aLogger.next(`creating components for [${aClass.typeDef.type.name}] ...`); // construct the base class const baseClass = _createAbstractComponent( aClass, aTools, aTemplate.abstractComponent, aOptions, aComponentReg, aLogger ); const typeClass = _createTypeComponent( aClass, aTools, aTemplate.typeComponent, aOptions, aLogger ); const typeInterface = _createTypeInterface( aClass, aTools, aTemplate.typeInterface, aOptions, aComponentReg, aLogger ); const typeDefinition = _createTypeDefinition( aClass.typeDef, aTools, aTemplate.typeDefinition, aOptions, aComponentReg, aLogger ); const typeHtml = _createTypeHtml( aClass, aTools, aTemplate.typeHtml, aOptions, aLogger ); const typeCss = _createTypeCss( aClass, aTools, aTemplate.typeCss, aOptions, aLogger ); // merge the results return merge( baseClass, typeClass, typeInterface, typeHtml, typeCss, typeDefinition ); } /** * Construct the base components * * @param aTools * @param aSource * @param aTemplate * @param aMatcher * @param aOptions * @param aLogger */ function _createComponents( aTools: WchTools, aTemplate: Templates, aMatcher: TypeMatcher, aOptions: Options, aCmpReg: ComponentRegistry, aLogger: Subject ): Observable { // log this aLogger.next(`createComponents [${Object.keys(aTools.types)}] ...`); // iterate return of(...Object.keys(aTools.types)).pipe( map(id => aTools.types[id]), filter(aMatcher), mergeMap(type => aCmpReg.findComponentClass(type)), mergeMap(clz => _createComponent(clz, aTools, aTemplate, aOptions, aCmpReg, aLogger) ), tap(noop, undefined, () => aLogger.next('createComponents done.')) ); } function _createLayoutComponent( aClass: LayoutClass, aTools: WchTools, aTemplate: Templates, aOptions: Options, aLayoutReg: LayoutRegistry, aLogger: Subject ): Observable { // cycle check if (!aLayoutReg.registerLayout(aClass.layout)) { // do not create a layout, twice return empty(); } // override flag const bOverride = !!aOptions.override; // code const dstLayoutFile = join(aClass.folder, aClass.layoutFile); const layoutCtx = _createLayoutComponentContext(aClass, aTools, aOptions); const layoutModuleCtx = _createLayoutModuleContext(aClass, aTools, aOptions); const layoutContent = aTemplate.layoutComponent.pipe( map(tmp => tmp(layoutCtx)) ); const layoutModule = aTemplate.layoutModule.pipe( map(tmp => tmp(layoutModuleCtx)) ); const layoutFile = layoutContent.pipe( mergeMap(data => writeTextFileSafe(dstLayoutFile, data, bOverride)) ); // log this aLogger.next(`layoutComponent for ${aClass.type.name} => ${dstLayoutFile}`); // module const dstModuleFileName = join(aClass.folder, aClass.moduleFile); const layoutModuleFile = layoutModule.pipe( mergeMap(data => writeTextFileSafe(dstModuleFileName, data, bOverride)) ); // html const dstHtmlFile = join(aClass.folder, aClass.layoutTemplateFile); const htmlCtx = _createLayoutHtmlContext(aClass, aTools, aOptions); const htmlContent = aTemplate.layoutHtml.pipe( mergeMap(tmp => rxTidy(tmp(htmlCtx).replace(/^\s*[\r\n]/gm, ''))) ); const htmlFile = htmlContent.pipe( mergeMap(data => writeTextFileSafe(dstHtmlFile, data, bOverride)) ); // log this aLogger.next(`layoutTemplate for ${aClass.type.name} => ${dstHtmlFile}`); // css const dstCssFile = join(aClass.folder, aClass.layoutStyleFile); const cssCtx = _createLayoutCssContext(aClass, aTools); const cssContent = aTemplate.layoutCss.pipe(map(tmp => tmp(cssCtx))); const cssFile = cssContent.pipe( mergeMap(data => writeTextFileSafe(dstCssFile, data, bOverride)) ); // log this aLogger.next(`layoutStyles for ${aClass.type.name} => ${dstCssFile}`); /* // prepare the context const ctx = _createTypeComponentContext(aType, aTools); // target name const fileName = _camelCase('type ' + aType.name) + '.ts'; // execute the template const newContent = aTemplate.map(tmp => tmp(ctx)); // write the content return combineLatest(src, newContent) .mergeMap(([path, data]) => writeFile(join(path, fileName), data)); */ // handle the module file option const files: Observable[] = [layoutFile, htmlFile, cssFile]; if (aOptions.modules) { files.push(layoutModuleFile); // log this aLogger.next( `layoutModule for ${aClass.type.name} => ${dstModuleFileName}` ); } // merge return merge(...files); } /** * Get all layouts for a particular layout mapping * * @param aMapping the mapping * @param aTools the tools * * @return the observable with the layouts */ function _getLayouts(aMapping: any, aTools: WchTools): Observable { const mappings = aMapping.mappings as any[]; return Observable.create((observer: Observer) => { mappings.forEach(mapping => { const layouts = mapping.layouts as any[]; layouts.forEach(layout => observer.next(aTools.layouts[layout.id])); }); // done observer.complete(); }); } function _getAllLayouts(aMapping: any, aTools: WchTools): Observable { // iterate return of(...Object.keys(aTools.layoutMappings)).pipe( map(id => aTools.layoutMappings[id]), mergeMap(mapping => _getLayouts(mapping, aTools)), filter(layout => layout != null && layout.templateType === 'angular'), distinct(layout => layout.id), // is name unique?? distinct(layout => layout.name) ); } function _createLayout( aMapping: any, aTools: WchTools, aTemplate: Templates, aOptions: Options, aLayoutReg: LayoutRegistry, aLogger: Subject ): Observable { // decode the type const typeId = aMapping.type.id; const type = aTools.types[typeId]; // check if (!type) { return empty(); } // check the layouts return _getLayouts(aMapping, aTools).pipe( filter(layout => layout != null && layout.templateType === 'angular'), distinct(layout => layout.id), // is name unique?? distinct(layout => layout.name), // resolve the layout class mergeMap(layout => aLayoutReg.findLayoutClass(layout, aMapping, type)), // produce the layout mergeMap(clz => _createLayoutComponent( clz, aTools, aTemplate, aOptions, aLayoutReg, aLogger ) ) ); } const regex = /(?:.|\n|\r)*@LayoutComponent(?:.|\n|\r)*@Component(?:.|\n|\r)*class\s+(\w+)\s+(?:.|\n|\r)*/; const layoutComponent = /@LayoutComponent/; const component = /@Component/; const className = /class\s+(\w+)\s+/; function _stripComment(aLine: string): string { // replace all after "//" const idx = aLine.indexOf('//'); const line = idx >= 0 ? aLine.substr(0, idx) : aLine; // replace /* */ like comments return line.replace(/(\/\*.*?\*\/)/g, ''); } function _stripSuffix(aValue: string): string { const idx = aValue.lastIndexOf('.'); if (idx >= 0) { return aValue.substr(0, idx); } return aValue; } const count = 0; interface ClassDescriptor { root: string; path: string; class: string; abs: string; } interface ModuleDescriptor { root: string; path: string; class: string; abs: string; } function _moduleDescriptorFromClassDescriptor( aClassDescriptor: ClassDescriptor ): Observable { // get the name of the module const absPath = aClassDescriptor.abs; if (absPath.endsWith('Layout.ts')) { // extract const parsedPath = parse(absPath); const layoutBase = parsedPath.name; const moduleBase = layoutBase.substr(0, layoutBase.length - 'Layout'.length) + 'Module'; const moduleName = moduleBase + '.ts'; const layoutPath = aClassDescriptor.path; const modulePath = layoutPath.substr(0, layoutPath.length - 'Layout'.length) + 'Module'; // test if that exists const absModulePath = join(parsedPath.dir, moduleName); // test if it exist return rxStats(absModulePath).pipe( catchError(error => empty()), mapTo({ root: parsedPath.dir, path: modulePath, class: upperFirst(moduleBase), abs: absModulePath }) ); } // nothing return empty(); } function _extractClass(aFile: FileDescriptor): Observable { // match by line const rs = createReadStream(aFile.absPath, 'utf-8'); // parse the lines return rxReadLines(rs).pipe( map(_stripComment), skipWhile(line => !layoutComponent.test(line)), skipWhile(line => !component.test(line)), filter(line => className.test(line)), map(line => ({ root: aFile.root, path: './' + _stripSuffix(replace(aFile.relPath, /\\/g, '/')), class: (className.exec(line) as string[])[1], abs: aFile.absPath })) ); } function _isRelative(aPath: string): boolean { return !!aPath && (aPath.startsWith('./') || aPath.startsWith('../')); } interface BaseClassCycle { [absPath: string]: ClassDescriptor; } function _distinctClassDescriptor(aDesc: ClassDescriptor): string { return aDesc.abs; } function _doExtractBaseClass( aConfig: ClassDescriptor, aCycle: BaseClassCycle ): Observable { // test if we know this if (aCycle[aConfig.abs]) { return empty(); } aCycle[aConfig.abs] = aConfig; // match by line const rs = createReadStream(aConfig.abs, 'utf-8'); const parsedPath = parse(aConfig.abs); // parse the lines const rxDoc = rxReadLines(rs).pipe( toArray(), map(lines => lines.join('\n')), shareReplay() ); // find the base classes const rgBaseClasses = new RegExp( `class\\s+${escapeStringRegexp(aConfig.class)}\\s+extends\\s+([^{]+)`, 'g' ); const rxBaseClasses = rxDoc.pipe( map(doc => rgBaseClasses.exec(doc)), filter(res => !!res && res.length > 1), map((res: RegExpExecArray) => res[1]), mergeMap(res => from(res.split(',').map(cls => cls.trim()))), mergeMap(cls => { // locate the import location const rgImport = new RegExp( `import\\s+{[\\s,\\w]*${escapeStringRegexp( cls )}[\\s,\\w]*}\\s+from\\s+['"]([^'"]+)['"]`, 'g' ); return rxDoc.pipe( mergeMap(doc => { const imp = rgImport.exec(doc); if (!!imp && imp.length > 1) { // extract the path const relPath = imp[1].trim(); if (_isRelative(relPath)) { // resolve against the current path const absImport = normalize( join(parsedPath.dir, relPath + '.ts') ); const relImport = './' + _stripSuffix( replace(relative(aConfig.root, absImport), /\\/g, '/') ); // recurse return _doExtractBaseClass( { root: aConfig.root, path: relImport, class: cls, abs: absImport }, aCycle ).pipe(distinct(_distinctClassDescriptor)); } } // nothing return empty(); }) ); }) ); // done return rxBaseClasses.pipe(startWith(aConfig)); } function _createOverview( aSource: Observable, aTemplate: Templates, aOptions: Options, aLogger: Subject ): Observable { // done return defer(() => { // log this aLogger.next(`_createOverview ...`); // read all typescript files const tsFiles = aSource.pipe( tap(src => aLogger.next(`_createOverview [src=${src}].`)), mergeMap(walkFiles), filter(f => f.stats.isFile()), distinct(f => f.absPath), filter(f => f.absPath.endsWith('.ts')) ); // read the class names of these files const rxClasses = tsFiles.pipe( mergeMap(_extractClass), tap(c => aLogger.next(`Class [${c.class}], [${c.path}].`)), share() ); // read the layouts const rxSortedClasses: Observable = rxClasses.pipe( toArray(), // order the imports map(a => a.sort((l, r) => l.class.localeCompare(r.class))), tap(noop, undefined, () => aLogger.next(`files done.`)), shareReplay() ); const rxModuleClasses: Observable = rxClasses.pipe( mergeMap(_moduleDescriptorFromClassDescriptor), tap(c => aLogger.next(`Module [${c.class}].`)), shareReplay() ); const rxSortedModuleClasses: Observable< ClassDescriptor[] > = rxModuleClasses.pipe( toArray(), // order the imports map(a => a.sort((l, r) => l.class.localeCompare(r.class))), tap(noop, undefined, () => aLogger.next(`files done.`)), shareReplay() ); const baseCycles: BaseClassCycle = {}; // imports const rxSortedImports = rxClasses.pipe( mergeMap(cls => _doExtractBaseClass(cls, baseCycles)), distinct(_distinctClassDescriptor), toArray(), // order the imports map(a => a.sort((l, r) => l.class.localeCompare(r.class))), tap(noop, undefined, () => aLogger.next(`base classes done.`)), shareReplay() ); // template const overviewTemplate = aTemplate.layoutOverview; const overviewRes = combineLatest(overviewTemplate, rxSortedClasses).pipe( map(([tmp, ctx]) => tmp(ctx)) ); const exportTemplate = aTemplate.layoutExports; const exportRes = combineLatest(exportTemplate, rxSortedImports).pipe( map(([tmp, ctx]) => tmp(ctx)) ); const moduleOverviewTemplate = aTemplate.moduleOverview; const moduleOverviewRes = combineLatest( moduleOverviewTemplate, rxSortedModuleClasses ).pipe(map(([tmp, ctx]) => tmp(ctx))); const moduleExportTemplate = aTemplate.moduleExports; const moduleExportRes = combineLatest( moduleExportTemplate, rxSortedModuleClasses ).pipe(map(([tmp, ctx]) => tmp(ctx))); // save overview const overviewFile = aSource.pipe(map(src => join(src, LAYOUTS_FILE))); const overviewWritten = combineLatest(overviewFile, overviewRes).pipe( mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true)) ); // save exports const exportsFile = aSource.pipe( map(src => join(src, LAYOUTS_EXPORT_FILE)) ); const exportsWritten = combineLatest(exportsFile, exportRes).pipe( mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true)) ); // save overview const modulesOverviewFile = aSource.pipe( map(src => join(src, MODULES_FILE)) ); const modulesOverviewWritten = combineLatest( modulesOverviewFile, moduleOverviewRes ).pipe(mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true))); // save exports const moduleExportsFile = aSource.pipe( map(src => join(src, MODULES_EXPORT_FILE)) ); const moduleExportsWritten = combineLatest( moduleExportsFile, moduleExportRes ).pipe(mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true))); // total files const total = [overviewWritten, exportsWritten]; aLogger.next(`Wrting modules [${aOptions.modules}] ...`); // add modules if (aOptions.modules) { total.push(modulesOverviewWritten, moduleExportsWritten); } return merge(...total).pipe( tap(noop, undefined, () => aLogger.next('createOverview done.')) ); }); } function _isNotNil(aMapping: any): boolean { return !!aMapping; } function _getTypeFromMapping(aMapping: any, aTools: WchTools): any { // get the ID if (aMapping.type && aMapping.type.id) { // get the type return aTools.types[aMapping.type.id]; } } function _createLayouts( aTools: WchTools, aSource: Observable, aTemplate: Templates, aMatcher: TypeMatcher, aOptions: Options, aLayoutReg: LayoutRegistry, aLogger: Subject ): Observable { // log this aLogger.next(`createLayouts [${Object.keys(aTools.layoutMappings)}]`); // iterate const layouts = of(...Object.keys(aTools.layoutMappings)).pipe( map(id => aTools.layoutMappings[id]), filter(_isNotNil), filter(mapping => aMatcher(_getTypeFromMapping(mapping, aTools))), mergeMap(mapping => _createLayout(mapping, aTools, aTemplate, aOptions, aLayoutReg, aLogger) ) ); const overview = _createOverview(aSource, aTemplate, aOptions, aLogger); // concat return concat(layouts, overview).pipe( tap(noop, undefined, () => aLogger.next('createLayouts done.')) ); } function _rewriteAppModule( aLayoutExports: string, aModuleFile: string, aOptions: Options, aLogger: Subject ): Observable { // just dispatch return !aOptions.skipRegistration ? addLayoutToModule(aLayoutExports, aModuleFile).pipe( catchError(() => empty()) ) : empty(); } function _matches( aName: string, aMatcher: Matcher, aLogger: Subject ): boolean { // check const result = aMatcher(aName); aLogger.next(`Matching [${aName}] results in [${result}].`); return result; } /** * Selects components by applying a matching function * * @param aMatcher the matcher * @param aLogger the logger */ function _byMatcher(aMatcher: Matcher, aLogger: Subject): TypeMatcher { // execute the check return (aType: any) => aType && aType.name && _matches(aType.name, aMatcher, aLogger); } /** * Generates angular components based on type and layout mapping definitions * * create components --data DATA_FOLDER --src SOURCE_CODE_FOLDER */ export class CreateComponentsCommand extends AbstractCommand implements Command { static readonly TYPE = 'components'; static readonly COMMAND = CMD_CREATE; constructor(private options: Options) { super(options); } printHelp(): void { // log the command line console.log( CreateComponentsCommand.COMMAND, CreateComponentsCommand.TYPE, `--${OPTION_SOURCE} `, `--${OPTION_DATA} `, `--${OPTION_MODULE} `, `[--${OPTION_MODULES}]`, `[--${OPTION_FLAT}]`, `[--${OPTION_SCSS}]`, `[--${OPTION_EDITABLE}]`, `[--${OPTION_VERBOSE}]` ); // log the parameters console.log(`--${OPTION_SOURCE} `, ':', HELP_COMPONENT_SOURCE); console.log(`--${OPTION_DATA} `, ':', HELP_DATA_DIRECTORY); console.log(`--${OPTION_MODULE} `, ':', HELP_MODULE_FILE); console.log(`--${OPTION_MODULES}`, ':', HELP_MODULES); console.log(`--${OPTION_FLAT}`, ':', HELP_FLAT); console.log(`--${OPTION_SCSS}`, ':', HELP_SCSS); console.log(`--${OPTION_EDITABLE}`, ':', HELP_EDITABLE); console.log(`--${OPTION_VERBOSE}`, ':', HELP_VERBOSE); } process(): Observable { return defer(() => { // current options const opts = this.options; // the directories const dataDirOpt = this.getDataDir(); const srcDirOpt = optNormalizeDir( firstOf(opts.src) || join(this.getAppDir(), 'app') ); // the mkdirp calls const mkdirp = rxMkDirs(); // fallback source directories const srcDirs = (arrayOf(opts.src) || [srcDirOpt]).map(optNormalizeDir); const rxSrcDir = mkdirp(srcDirOpt); const rxSrcDirs = concat(...srcDirs.map(dir => mkdirp(dir))).pipe( toArray(), shareReplay(1) ); // module file const moduleFile = opts.module || join(srcDirOpt, APP_MODULE_FILE); const layoutsFile = join(srcDirOpt, LAYOUTS_FILE); // logger const logger = this.getLogger(); // the compiles template const templates = _createTemplate(opts, logger); // matcher const matcher: Matcher = wchToolsGetMatcher(this.options); const typeMatcher: TypeMatcher = _byMatcher(matcher, logger); // the tools const rxWchTools = this.getWchTools(); // log the name logger.next( `Data dir [${dataDirOpt}], dst dir [${srcDirOpt}], src dirs [${srcDirs}], ngModule [${moduleFile}], layouts [${layoutsFile}].` ); // the registries const typeReg = new TypeRegistry(rxSrcDirs, opts, mkdirp, logger); const cmpReg = new ComponentRegistry( rxSrcDirs, opts, mkdirp, typeReg, logger ); const layoutReg = new LayoutRegistry( rxSrcDirs, opts, mkdirp, cmpReg, logger ); // attach to the tools return rxWchTools.pipe( mergeMap(aWchTools => concat( _createComponents( aWchTools, templates, typeMatcher, opts, cmpReg, logger ), _createLayouts( aWchTools, rxSrcDir, templates, typeMatcher, opts, layoutReg, logger ), _rewriteAppModule(layoutsFile, moduleFile, opts, logger) ) ), distinct() ); }); } } export { _createOverview as createComponentOverview, _createTemplate as createComponentTemplates, _rewriteAppModule as rewriteAppModule, _createAbstractComponentContext as createAbstractComponentContext, _createTypeInterfaceContext as createTypeInterfaceContext };