/* Copyright IBM Corp. 2017 */ /*-------------------- Imports, consts, helpers -------------------*/ 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, UnaryFunction } from 'rxjs'; import { distinct, filter, map, mapTo, mergeMap, share, shareReplay, startWith, tap, toArray } from 'rxjs/operators'; // import { LayoutClass, LayoutRegistry } from '../utils/layout.reg'; import { rxMkDirs } from '../utils/rx.copy'; import { CMD_CREATE, OPTION_DATA, OPTION_EDITABLE, OPTION_FLAT, OPTION_MODULE, OPTION_MODULES, OPTION_SCSS, OPTION_SOURCE, OPTION_VERBOSE } from './../constants'; import { arrayOf, firstOf } from '../utils/file.utils'; import { Options, optNormalizeDir } from './../options'; 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, walkFiles } from './../utils/rx.walk'; import { camelCase, kebabCase } from './../utils/transliteration'; 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'); 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']; /*---------------------------------------------------------------*/ /*----------------------- Actual code ---------------------------*/ // - why is logger being passed into every function... just make a global singleton and use it directly // - why is options being passed into every function... just make a global singleton and use it directly? the values won't change // for passing in the dictionary of templates as one object to funcs interface Templates { appRegisterComponents: Observable; indexExports: Observable; layoutJsx: Observable; layoutCss: Observable; } // main driver function (ENTRY POINT FOR CreateReactCommand class) // calls _getTypeFromMapping() with filter -- get corresponding layout info // calls _createLayout() -- creates the .jsx and .scss files // calls _createOverview() -- creates/udpates the app.jsx and layouts/index.jsx files 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 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]; } } 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( // return concat(overview).pipe( tap(noop, undefined, () => aLogger.next('createLayouts done.')) ); } // main layout creation function // calls _getLayouts() -- essentially gets a list of all the layouts we need to create // calls _createLayoutComponent -- actually generates the .jsx/.scss files for a layout 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(); } /** * Get all layouts for a particular layout mapping */ 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(); }); } // check the layouts and create from templates if everything is fine 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, type)), // produce the layout mergeMap(clz => _createLayoutComponent(clz, aTools, aTemplate, aOptions, aLayoutReg, aLogger) ) ); } // generate the .jsx + .scss files for a layout 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; // jsx /** * Constructs the context to generate a layout sample */ function _createLayoutJsxContext( 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 { // VALUES PASSED INTO HBS TEMPLATE className: aClass.layoutClass, fileName: aClass.layoutRef, editable: !!aOptions.editable || false, elements: el }; } const jsxCtx = _createLayoutJsxContext(aClass, aTools, aOptions); const jsxContent = aTemplate.layoutJsx.pipe( mergeMap(tmp => rxTidy(tmp(jsxCtx).replace(/^\s*[\r\n]/gm, ''))) ); const dstJsxFile = join(aClass.jsxFolder, aClass.layoutTemplateFile); const jsxFile = jsxContent.pipe( mergeMap(data => writeTextFileSafe(dstJsxFile, data, bOverride)) ); // css function _createLayoutCssContext(aClass: LayoutClass, aTools: WchTools) { return { className: aClass.layoutClass }; } const cssCtx = _createLayoutCssContext(aClass, aTools); const cssContent = aTemplate.layoutCss.pipe(map(tmp => tmp(cssCtx))); const dstCssFile = join(aClass.cssFolder, aClass.layoutStyleFile); const cssFile = cssContent.pipe( mergeMap(data => writeTextFileSafe(dstCssFile, data, bOverride)) ); const files: Observable[] = [jsxFile, cssFile]; // merge return merge(...files); } /* generating app.jsx and layouts/index.jsx */ function _createOverview( aSource: Observable, aTemplate: Templates, aOptions: Options, aLogger: Subject ): Observable { // done return defer(() => { /*------------------------- Private consts and helpers used only by _createOverview() -----------------------------*/ interface ClassDescriptor { root: string; path: string; class: string; abs: string; } 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; } // read the class names of these files 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, fileName: _stripSuffix(replace(aFile.relPath, /\\/g, '/')).split('/').pop(), className: (className.exec(line) as string[])[1], // get relative location for import path importPath: ('./' + _stripSuffix(replace(aFile.relPath, /\\/g, '/'))), registrationPath: ('./' + LAYOUTS_PATH + '/' + _stripSuffix(replace(aFile.relPath, /\\/g, '/'))), // add '-layout' to the layout selector if it doesn't end with list lol layoutSelector: kebabCase((className.exec(line) as string[])[1]) + (kebabCase((className.exec(line) as string[])[1]).endsWith('list') ? '' : '-layout'), })) ); } function _isRelative(aPath: string): boolean { return !!aPath && (aPath.startsWith('./') || aPath.startsWith('../')); } interface BaseClassCycle { [absPath: string]: ClassDescriptor; } function _distinctClassDescriptor(aDesc: ClassDescriptor): string { return aDesc.abs; } // main helper function 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 + '.jsx') ); 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)); } /*----------------------------------------------------*/ // log this aLogger.next(`_createOverview ...`); // read all jsx files // only look in aSource/src/LAYOUTS_PATH const jsxSource = aSource.pipe(map(src => join(src, 'src', LAYOUTS_PATH))); const jsxFiles = jsxSource.pipe( tap(src => aLogger.next(`_createOverview [src=${src}].`)), mergeMap(walkFiles), filter(f => f.stats.isFile()), distinct(f => f.absPath), filter(f => f.absPath.endsWith('.jsx')) ); // read the class names of these files const rxClasses = jsxFiles.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() ); // imports const baseCycles: BaseClassCycle = {}; 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() ); // overview and export template generation + saving const overviewTemplate = aTemplate.appRegisterComponents; const overviewRes = combineLatest(overviewTemplate, rxSortedClasses).pipe( map(([tmp, ctx]) => tmp(ctx)) ); const REGISTRATION_FILE = "registration.js"; const overviewFile = aSource.pipe(map(src => join(src, 'src', REGISTRATION_FILE))); const overviewWritten = combineLatest(overviewFile, overviewRes).pipe( mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true)) ); const exportTemplate = aTemplate.indexExports; const exportRes = combineLatest(exportTemplate, rxSortedImports).pipe( map(([tmp, ctx]) => tmp(ctx)) ); const EXPORT_FILE = "index.js" // TODO: make this layouts/index.js const exportsFile = aSource.pipe(map(src => join(src, 'src', LAYOUTS_PATH, EXPORT_FILE))); const exportsWritten = combineLatest(exportsFile, exportRes).pipe( mergeMap(([dst, data]) => writeTextFileSafe(dst, data, true)) ); // total files const total = [overviewWritten, exportsWritten]; return merge(...total).pipe( tap(noop, undefined, () => aLogger.next('createOverview done.')) ); }); } // Actual command class // does the setup by making templates, matcher, tools, registries // calls _createLayouts() -- does everything else // (ACTUAL MAIN ENTRY POINT BY COMMAND FACTORY) export class CreateReactCommand extends AbstractCommand implements Command { static readonly TYPE = 'react_components'; static readonly COMMAND = CMD_CREATE; constructor(private options: Options) { super(options); } printHelp(): void { // log the command line console.log( CreateReactCommand.COMMAND, CreateReactCommand.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 mkdirp calls const mkdirp = rxMkDirs(); // the directories // const srcDirOpt = optNormalizeDir('.'); const srcDirOpt = optNormalizeDir(firstOf(opts.src) || '.'); console.log("OPTS.SRC: ", opts.src); console.log("SRC DIR OPT: ", srcDirOpt); // fallback source directories // const srcDirs = (['.']).map(optNormalizeDir); const srcDirs = (arrayOf(opts.src) || [srcDirOpt]).map(optNormalizeDir); console.log("SRC DIRS: ", srcDirs); const rxSrcDir = mkdirp(srcDirOpt); const rxSrcDirs = concat(...srcDirs.map(dir => mkdirp(dir))).pipe(toArray(), shareReplay(1)); const logger = this.getLogger(); const rxWchTools = this.getWchTools(); // Compile the templates // the one-line additions (for each type) for registering the component and exporting const rxAppRegisterComponents = hbsCompile(join(ASSETS_BASE, 'react.registration.hbs')); const rxIndexExports = hbsCompile(join(ASSETS_BASE, 'react.index.hbs')); // jsx and scss templates const rxLayoutJsx = hbsCompile(join(ASSETS_BASE, 'react.jsx.hbs')); const rxLayoutCss = hbsCompile(join(ASSETS_BASE, 'react.scss.hbs')); // package into a Templates obj const templates: Templates = { appRegisterComponents: rxAppRegisterComponents, indexExports: rxIndexExports, layoutJsx: rxLayoutJsx, layoutCss: rxLayoutCss, } // matcher with type checking const matcher: Matcher = wchToolsGetMatcher(this.options); const typeMatcher: TypeMatcher = (aType: any) => aType && aType.name && matcher(aType.name); // the registries const layoutReg = new LayoutRegistry(rxSrcDirs, opts, mkdirp, logger); // attach to the tools return rxWchTools.pipe( mergeMap(aWchTools => _createLayouts(aWchTools, rxSrcDir, templates, typeMatcher, opts, layoutReg, logger), ), distinct() ); }); } } import { lowerFirst } from 'lodash'; import { toWords } from 'number-to-words'; import { sep } from 'path'; import { first, } from 'rxjs/operators'; import { rxFindDir } from '../utils/rx.dir'; function _getStyleExtension(aOptions: Options): string { return aOptions.scss ? '.scss' : '.css'; } function _kebabCase(aValue: string): string { return kebabCase(aValue); } const LEADING_DIGIT = /^(\d*)/g; const LAYOUTS_PATH = 'layouts'; const LAYOUT_SUFFIX = 'Layout'; 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 */ let name = upperFirst(_camelCase(fixed)); // remove 'Layout' from the end of component name if (name.endsWith(LAYOUT_SUFFIX)) { name = name.substr(0, name.length - LAYOUT_SUFFIX.length); } return name; } function _getLayoutPath(aLayout: any, aType: any, aOptions: Options): string { return LAYOUTS_PATH; // function _getPathForLayout(aName: string, aType: any, aOptions: Options): string { // // name of the layout folder segment // let layoutName = _createLayoutComponentName(aName); // // 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); // } // console.log("stuff:", _getPathForLayout(aLayout.name, aType, aOptions)); // return join(LAYOUTS_PATH, _getPathForLayout(aLayout.name, aType, aOptions)); } export interface LayoutClass { // directory for layout // folder: string; jsxFolder: string; cssFolder: string; // selection coordinates type: any; layout: any; // class names layoutClass: string; layoutTemplateFile: string; layoutStyleFile: string; // file references layoutRef: 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, aType: any, aLayout: any, aOptions: Options, aLogger: Subject): LayoutClass { // check for the path suffix const layoutPath = _getLayoutPath(aLayout, aType, aOptions); const layoutName = aLayout.name || aType.name; // component names console.log("LAYOUT NAME:", layoutName); const layoutBaseName = _createLayoutComponentName(layoutName); const layoutClass = layoutBaseName; // references const layoutRef = lowerFirst(layoutBaseName); // template and style const layoutTemplateFile = layoutRef + '.jsx'; // need this to be dependent on angular vs react const layoutStyleFile = layoutRef + _getStyleExtension(aOptions); // selector // const layoutSelector = _kebabCase(layoutClass); const layoutSelector = "is this even used lmao"; // the root folder // const folder = normalize(join(aDir, layoutPath)); const jsxFolder = normalize(join(aDir, 'src', layoutPath)); const cssFolder = normalize(join(aDir, 'styles', layoutPath)); console.log("ADIR:", aDir); const result: LayoutClass = { type: aType, layout: aLayout, jsxFolder, cssFolder, layoutClass, layoutRef: layoutRef, layoutTemplateFile, layoutStyleFile, layoutSelector, layoutTemplate: aLayout.template }; // log this console.log(`Type: ${aType.name}, LayoutClass: ${JSON.stringify(layoutClass)}, Jsx folder: ${jsxFolder}, Css folder: ${cssFolder}`); // 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, aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { return aSources.pipe( map(sources => sources[0]), mergeMap(aMkdirp) ); // // 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 => console.log(`Selected layout source directory [${dir}] for layout [${className}].`)) // ); } function _createLayoutClass( aLayout: any, aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // the type const type = aType; // locate the component directory return _getLayoutsDir(aLayout, type, aSources, aOptions, aMkdirp, aLogger).pipe( first(), map(dir => _initLayoutClass(dir, aType, aLayout, aOptions, aLogger)), mergeMap(clz => concat(aMkdirp(clz.jsxFolder), aMkdirp(clz.cssFolder)).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 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, aType: any): Observable { // the name const layoutId = aLayout.id; // check if we are already searching let rxRunning = this.layoutRegistry[layoutId]; if (!rxRunning) { // try to find the folder rxRunning = this.layoutRegistry[layoutId] = _createLayoutClass(aLayout, aType, this.aBaseFolders, this.aOptions, this.aMkdirp, this.aLogger); } // ok return rxRunning; } }