import { upperFirst } from 'lodash'; import { join, normalize, sep } from 'path'; import { Observable, Subject, UnaryFunction } from 'rxjs'; import { first, map, mapTo, mergeMap, shareReplay, tap } from 'rxjs/operators'; import { Options } from './../options'; import { camelCase, kebabCase } from './../utils/transliteration'; import { rxFindDir } from './rx.dir'; import { TypeClass, TypeRegistry } from './type.reg'; function _getStyleExtension(aOptions: Options): string { return aOptions.scss ? '.scss' : '.css'; } function _camelCase(aValue: string): string { return camelCase(aValue); } function _kebabCase(aValue: string): string { return kebabCase(aValue); } const COMPONENT_SUFFIX = 'Component'; const RENDERING_CONTEXT_SUFFIX = 'RenderingContext'; const COMPONENTS_PATH = 'components'; /** * Returns the directory name for the type * * @param aType the type * @return the directory name */ function _getPathForType(aType: any, aOptions: Options): string { // access the type properties const name: string = aType.name; const tags: string[] = aType.tags || []; // convert tags into segments const segments = aOptions.flat ? [] : tags.map(_kebabCase).sort(); segments.push(_kebabCase(name)); // return this path return segments.join(sep); } function _createAbstractComponentName(aTypeName: string): string { const name = upperFirst(_camelCase('abstract ' + aTypeName)); return name.endsWith(COMPONENT_SUFFIX) ? name : name + COMPONENT_SUFFIX; } function _createTypeInterfaceName(aTypeName: string): string { const name = upperFirst(_camelCase(aTypeName)); return name.endsWith(RENDERING_CONTEXT_SUFFIX) ? name : name + RENDERING_CONTEXT_SUFFIX; } function _createTypeComponentName(aTypeName: string): string { const name = upperFirst(_camelCase('type ' + aTypeName)); return name.endsWith(COMPONENT_SUFFIX) ? name : name + COMPONENT_SUFFIX; } function _getTypeComponentPath(aType: any, aOptions: Options): string { // prepend by component return join(COMPONENTS_PATH, _getPathForType(aType, aOptions)); } /** * Finds the directory tha the component should be created in. Tests * each potential source directory for an existing component file. If non exists, uses the * first source directory * * @param aType type * @param aSources sources directory * * @return the selected source directory */ function _getComponentsDir( aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // check for the path suffix const typePath = _getTypeComponentPath(aType, aOptions); const className = _createAbstractComponentName(aType.name); const relPath = join(typePath, _camelCase(className) + '.ts'); // map each source to a check return rxFindDir(aSources, relPath, aMkdirp).pipe( tap(dir => aLogger.next(`Selected component source directory [${dir}] for component [${className}].`)) ); } export interface ComponentClass { // directory for layout folder: string; // the type for the component typeDef: TypeClass; // class names abstractComponentClass: string; typeComponentClass: string; typeInterfaceClass: string; // file names abstractComponentFile: string; typeComponentFile: string; typeInterfaceFile: string; typeTemplateFile: string; typeStyleFile: string; // file references abstractComponentRef: string; typeComponentRef: string; typeInterfaceRef: string; // selector typeSelector: string; } /** * Constructs the relevant information for a components class * * @param aDir base directory * @param aType type object * @param aOptions options * @param aLogger logger * * @return the component object */ function _initComponentClass(aDir: string, aTypeDef: TypeClass, aOptions: Options, aLogger: Subject): ComponentClass { // type object const type = aTypeDef.type; // check for the path suffix const typePath = _getTypeComponentPath(type, aOptions); const typeName = type.name; // component names const abstractComponentClass = _createAbstractComponentName(typeName); const typeComponentClass = _createTypeComponentName(typeName); const typeInterfaceClass = _createTypeInterfaceName(typeName); // references const abstractComponentRef = _camelCase(abstractComponentClass); const typeComponentRef = _camelCase(typeComponentClass); const typeInterfaceRef = _camelCase(typeInterfaceClass); // file names const abstractComponentFile = abstractComponentRef + '.ts'; const typeComponentFile = typeComponentRef + '.ts'; const typeInterfaceFile = typeInterfaceRef + '.ts'; // template and style const typeTemplateFile = typeComponentRef + '.html'; const typeStyleFile = typeComponentRef + _getStyleExtension(aOptions); // selector const typeSelector = _kebabCase('app ' + typeComponentClass); // the base path const folder = normalize(join(aDir, typePath)); const result: ComponentClass = { typeDef: aTypeDef, folder, typeSelector, typeTemplateFile, typeStyleFile, abstractComponentRef, typeComponentRef, typeInterfaceRef, abstractComponentClass, typeComponentClass, typeInterfaceClass, abstractComponentFile, typeComponentFile, typeInterfaceFile }; // log this aLogger.next(`Type: ${typeName}, ComponentClass: ${JSON.stringify(typeComponentClass)}, Folder: ${folder}`); // ok return result; } function _createComponentClass( aTypeDef: TypeClass, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // locate the component directory return _getComponentsDir(aTypeDef.type, aSources, aOptions, aMkdirp, aLogger).pipe( first(), map(dir => _initComponentClass(dir, aTypeDef, aOptions, aLogger)), mergeMap(clz => aMkdirp(clz.folder).pipe(mapTo(clz))), shareReplay() ); } export class ComponentRegistry { private componentRegistry: { [typeId: string]: Observable } = {}; constructor( private aBaseFolders: Observable, private aOptions: Options, private aMkdirp: UnaryFunction>, private aTypeRegistry: TypeRegistry, private aLogger: Subject) { } registerType(aType: any): boolean { return this.aTypeRegistry.registerType(aType); } findTypeClass(aType: any): Observable { return this.aTypeRegistry.findTypeClass(aType); } findComponentClass(aType: any): Observable { // the name const typeId = aType.id; // check if we are already searching let rxRunning = this.componentRegistry[typeId]; if (!rxRunning) { // find the type const rxType = this.aTypeRegistry.findTypeClass(aType); // try to find the folder rxRunning = rxType.pipe( mergeMap(type => this.componentRegistry[typeId] = _createComponentClass(type, this.aBaseFolders, this.aOptions, this.aMkdirp, this.aLogger)) ); } // ok return rxRunning; } }