dyatko/arkit

View on GitHub
src/generator.ts

Summary

Maintainability
C
1 day
Test Coverage
import * as path from "path";
import {
  trace,
  warn,
  array,
  match,
  verifyComponentFilters,
  info
} from "./utils";
import {
  ComponentFilters,
  ComponentNameFormat,
  ComponentSchema,
  OutputSchema,
  Files,
  Component,
  Components,
  EMPTY_LAYER,
  Layers,
  ConfigBase
} from "./types";

export class Generator {
  private readonly config: ConfigBase;
  private readonly files: Files;

  constructor(config: ConfigBase, files: Files) {
    this.config = config;
    this.files = files;
  }

  generate(output: OutputSchema): Layers {
    info("Generating components...");
    const components = this.sortComponentsByName(
      this.resolveConflictingComponentNames(this.generateComponents(output))
    );
    trace(Array.from(components.values()));

    info("Generating layers...");
    return this.generateLayers(output, components);
  }

  protected generateComponents(output: OutputSchema): Components {
    const components = Object.keys(this.files).reduce(
      (components, filename) => {
        const filepath = filename.endsWith("**")
          ? path.dirname(filename)
          : filename;
        const schema = this.findComponentSchema(output, filepath);

        if (schema) {
          const name = this.getComponentName(filepath, schema);
          const file = this.files[filename];
          const imports = Object.keys(file.imports);
          const isClass = file.exports.some(exp => !!exp.match(/^[A-Z]/));

          components.set(filename, {
            name,
            filename,
            imports,
            isClass,
            isImported: false,
            type: schema.type,
            layer: EMPTY_LAYER
          });
        }

        return components;
      },
      new Map() as Components
    );

    for (const component of components.values()) {
      for (const potentialComponent of components.values()) {
        if (potentialComponent.imports.includes(component.filename)) {
          component.isImported = true;
          break;
        }
      }
    }

    return components;
  }

  protected generateLayers(
    output: OutputSchema,
    allComponents: Components
  ): Layers {
    const groups = array(output.groups) || [{}];
    const ungroupedComponents: Components = new Map(allComponents);
    const grouppedComponents = new Map<string, Component>();
    const layers: Layers = new Map();

    groups.forEach(group => {
      const layerType = group.type || EMPTY_LAYER;

      if (!layers.has(layerType)) {
        layers.set(layerType, new Set());
      }

      Array.from(ungroupedComponents.entries())
        .filter(([filename, component]) => {
          return verifyComponentFilters(
            group,
            component,
            this.config.directory
          );
        })
        .forEach(([filename, component]) => {
          component.layer = layerType;
          component.first = group.first;
          component.last = group.last;
          layers.get(layerType)!.add(component);
          grouppedComponents.set(component.filename, component);
          ungroupedComponents.delete(filename);
          return component;
        });
    });

    if (ungroupedComponents.size) {
      trace("Ungrouped components");
      trace(Array.from(ungroupedComponents.values()));
    }

    const filenamesFromFirstComponents = new Set<string>();

    for (const component of grouppedComponents.values()) {
      if (component.first) {
        this.collectImportedFilenames(
          component,
          grouppedComponents,
          filenamesFromFirstComponents
        );
      }
    }

    if (filenamesFromFirstComponents.size) {
      trace("Filenames from first components");
      trace(Array.from(filenamesFromFirstComponents));

      for (const [filename, component] of allComponents) {
        if (!filenamesFromFirstComponents.has(filename)) {
          for (const components of layers.values()) {
            components.delete(component);
          }

          ungroupedComponents.delete(filename);
          allComponents.delete(filename);
        }
      }
    }

    if (ungroupedComponents.size) {
      trace("Ungrouped components leftovers");
      trace(Array.from(ungroupedComponents.values()));
    }

    return layers;
  }

  private collectImportedFilenames(
    component: Component,
    components: Components,
    filenames: Set<string>
  ) {
    if (filenames.has(component.filename)) return;

    filenames.add(component.filename);

    if (!component.last) {
      component.imports.forEach(importedFilename => {
        const importedComponent = components.get(importedFilename);
        if (importedComponent) {
          this.collectImportedFilenames(
            importedComponent,
            components,
            filenames
          );
        }
      });
    } else {
      component.imports = [];
    }
  }

  protected resolveConflictingComponentNames(
    components: Components
  ): Components {
    const componentsByName: { [name: string]: Component[] } = {};

    for (const component of components.values()) {
      componentsByName[component.name] = componentsByName[component.name] || [];
      componentsByName[component.name].push(component);
    }

    for (const name in componentsByName) {
      const components = componentsByName[name];
      const isIndex = name === "index";
      const shouldPrefixWithDirectory = components.length > 1 || isIndex;

      if (shouldPrefixWithDirectory) {
        for (const component of components) {
          const componentPath = path.dirname(component.filename);
          const dir =
            componentPath !== this.config.directory
              ? path.basename(componentPath)
              : "";

          component.name =
            isIndex && dir ? dir : path.join(dir, component.name);
        }
      }
    }

    return components;
  }

  protected sortComponentsByName(components: Components): Components {
    const sortedComponents: Components = new Map(
      Array.from(components.entries()).sort((a, b) =>
        a[1].name.localeCompare(b[1].name)
      )
    );

    for (const component of components.values()) {
      component.imports = component.imports
        .filter(importedFilename => components.has(importedFilename))
        .sort((a, b) => {
          const componentA = components.get(a)!;
          const componentB = components.get(b)!;
          return componentA.name.localeCompare(componentB.name);
        });
    }

    return sortedComponents;
  }

  protected findComponentSchema(
    output: OutputSchema,
    filename: string
  ): ComponentSchema | undefined {
    const componentSchemas = this.config.final.components as ComponentSchema[];
    const componentSchema = componentSchemas.find(componentSchema => {
      const outputFilters: ComponentFilters[] = array(output.groups) || [];
      const includedInOutput =
        !outputFilters.length ||
        outputFilters.some(outputFilter =>
          verifyComponentFilters(
            outputFilter,
            componentSchema,
            this.config.directory
          )
        );

      if (includedInOutput) {
        return (
          !!componentSchema.patterns &&
          match(
            path.relative(this.config.directory, filename),
            componentSchema.patterns
          )
        );
      } else {
        return false;
      }
    });

    if (!componentSchema) {
      warn(`Component schema not found: ${filename}`);
    }

    return componentSchema;
  }

  protected getComponentName(
    filename: string,
    componentConfig: ComponentSchema
  ): string {
    const nameFormat = componentConfig.format;

    if (nameFormat === ComponentNameFormat.FULL_NAME) {
      return path.basename(filename);
    }

    return path.basename(filename, path.extname(filename));
  }
}