import { upperFirst } from 'lodash'; import { toWords } from 'number-to-words'; 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 { ComponentClass, ComponentRegistry } from './component.reg'; import { rxFindDir } from './rx.dir'; const COMPONENT_SUFFIX = 'Component'; 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 LEADING_DIGIT = /^(\d*)/g; const LAYOUT_SUFFIX = 'Layout'; const MODULE_SUFFIX = 'Module'; const LAYOUTS_PATH = 'layouts'; const MODULES_PATH = 'modules'; function _createLayoutComponentName(aName: string): string { // trim const trimmedName = aName.trim(); const fixed = trimmedName.replace(LEADING_DIGIT, (m, p) => (p && (p.length > 0)) ? toWords(p) + ' ' : ''); /** check if the layout component starts with a number, in this case * convert the number to a string */ const name = upperFirst(_camelCase(fixed)); return name.endsWith(LAYOUT_SUFFIX) ? name : name + LAYOUT_SUFFIX; } function _createLayoutModuleName(aName: string): string { // derive from layout name const layoutName = _createLayoutComponentName(aName); // replace the word 'layout' return layoutName.substr(0, layoutName.length - LAYOUT_SUFFIX.length) + MODULE_SUFFIX; } function _getPathForLayoutModule(aName: string, aType: any, aOptions: Options): string { // name of the layout folder segment let moduleName = _createLayoutModuleName(aName); if (moduleName.endsWith(MODULE_SUFFIX)) { moduleName = moduleName.substr(0, moduleName.length - MODULE_SUFFIX.length); } // layout folder const moduleFolder = _kebabCase(moduleName); // access the type properties const tags: string[] = aType.tags || []; // convert tags into segments const segments = aOptions.flat ? [] : tags.map(_kebabCase).sort(); segments.push(moduleFolder); // return this path return segments.join(sep); } function _getPathForLayout(aName: string, aType: any, aOptions: Options): string { // name of the layout folder segment let layoutName = _createLayoutComponentName(aName); if (layoutName.endsWith(LAYOUT_SUFFIX)) { layoutName = layoutName.substr(0, layoutName.length - LAYOUT_SUFFIX.length); } // layout folder const layoutFolder = _kebabCase(layoutName); // access the type properties const tags: string[] = aType.tags || []; // convert tags into segments const segments = aOptions.flat ? [] : tags.map(_kebabCase).sort(); segments.push(layoutFolder); // return this path return segments.join(sep); } function _getLayoutModulePath(aLayout: any, aType: any, aOptions: Options): string { // join return join(MODULES_PATH, _getPathForLayoutModule(aLayout.name, aType, aOptions)); } function _getLayoutPath(aLayout: any, aType: any, aOptions: Options): string { // join return join(LAYOUTS_PATH, _getPathForLayout(aLayout.name, aType, aOptions)); } export interface LayoutClass { // directory for layout folder: string; // component class component: ComponentClass; // selection coordinates type: any; layout: any; mapping: any; // class names layoutClass: string; moduleClass: string; // file names layoutFile: string; moduleFile: string; layoutTemplateFile: string; layoutStyleFile: string; // file references layoutRef: string; moduleRef: string; // selector layoutSelector: string; layoutTemplate: 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 _initLayoutClass(aDir: string, aCmpClass: ComponentClass, aLayout: any, aMapping: any, aOptions: Options, aLogger: Subject): LayoutClass { // access the type object const type = aCmpClass.typeDef.type; // check for the path suffix const layoutPath = _getLayoutPath(aLayout, type, aOptions); const layout = aLayout; const mapping = aMapping; const typeName = type.name; const layoutName = layout.name || typeName; const component = aCmpClass; // component names const layoutBaseName = _createLayoutComponentName(layoutName); const layoutClass = layoutBaseName + COMPONENT_SUFFIX; const moduleClass = _createLayoutModuleName(layoutName); // references const layoutBaseRef = _camelCase(layoutBaseName); const layoutRef = layoutBaseRef; const moduleRef = _camelCase(moduleClass); // file names const layoutFile = layoutRef + '.ts'; const moduleFile = moduleRef + '.ts'; // template and style const layoutTemplateFile = layoutBaseRef + '.html'; const layoutStyleFile = layoutBaseRef + _getStyleExtension(aOptions); // selector const layoutSelector = _kebabCase('app ' + layoutClass); const layoutTemplate = layout.template; // the base path for the layout const folder = normalize(join(aDir, layoutPath)); const result: LayoutClass = { component, type, mapping, layout, folder, layoutClass, moduleClass, layoutRef, moduleRef, layoutFile, moduleFile, layoutTemplateFile, layoutStyleFile, layoutSelector, layoutTemplate }; // log this aLogger.next(`Type: ${typeName}, LayoutClass: ${JSON.stringify(layoutClass)}, Folder: ${folder}`); // ok return result; } interface LayoutMap { byId: { [id: string]: any }; byName: { [id: string]: any }; dirById: { [id: string]: string }; } /** * 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 _getLayoutsDir( aLayout: any, aMapping: any, aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // check for the path suffix const typePath = _getLayoutPath(aLayout, aType, aOptions); const className = _createLayoutComponentName(aLayout.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 layout source directory [${dir}] for layout [${className}].`)) ); } function _createLayoutClass( aLayout: any, aMapping: any, aSources: Observable, aCmpClass: ComponentClass, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // the type const type = aCmpClass.typeDef.type; // locate the component directory return _getLayoutsDir(aLayout, aMapping, type, aSources, aOptions, aMkdirp, aLogger).pipe( first(), map(dir => _initLayoutClass(dir, aCmpClass, aLayout, aMapping, aOptions, aLogger)), mergeMap(clz => aMkdirp(clz.folder).pipe(mapTo(clz))), shareReplay() ); } export class LayoutRegistry { private layoutMap: LayoutMap = { byId: {}, byName: {}, dirById: {} }; private layoutRegistry: { [layoutName: string]: Observable } = {}; constructor( private aBaseFolders: Observable, private aOptions: Options, private aMkdirp: UnaryFunction>, private aComponentRegistry: ComponentRegistry, private aLogger: Subject) { } registerLayout(aLayout: any): boolean { const layout = aLayout; const lid = layout.id; const lname = layout.name; if (this.layoutMap.byId[lid] || this.layoutMap.byName[lname]) { // do not create a layout, twice return false; } // register this.layoutMap.byId[lid] = layout; this.layoutMap.byName[lname] = layout; // ok return true; } findLayoutClass( aLayout: any, aMapping: any, aType: any): Observable { // the name const layoutId = aLayout.id; // check if we are already searching let rxRunning = this.layoutRegistry[layoutId]; if (!rxRunning) { // find the component const rxComponent = this.aComponentRegistry.findComponentClass(aType); // try to find the folder rxRunning = this.layoutRegistry[layoutId] = rxComponent.pipe(mergeMap(cmp => _createLayoutClass(aLayout, aMapping, this.aBaseFolders, cmp, this.aOptions, this.aMkdirp, this.aLogger))); } // ok return rxRunning; } }