SpencerCDixon/redux-cli

View on GitHub
src/models/blueprint.js

Summary

Maintainability
A
0 mins
Test Coverage
import path from 'path';
import _ from 'lodash';
import walkSync from 'walk-sync';
import fs from 'fs';

import { fileExists } from '../util/fs';
import mixin from '../util/mixin';
import { normalizeCasing } from '../util/text-helper';
import FileInfo from './file-info';
import config from '../config';

const { basePath } = config;

export default class Blueprint {
  constructor(blueprintPath) {
    this.path = blueprintPath;
    this.name = path.basename(blueprintPath);
    this.command = this.command || {}; // default if not set by mixin
  }

  // HOOK: this can be overridden
  description() {
    return `Generates a new ${this.name}`;
  }

  // HOOK: that can be overridden.  Defaults to look in <blueprint-name>/files.
  filesPath() {
    return path.join(this.path, 'files');
  }

  files() {
    if (this._files) {
      return this._files;
    }

    let filesPath = this.filesPath();

    if (fileExists(filesPath)) {
      this._files = walkSync(filesPath);
    } else {
      this._files = [];
    }
    return this._files;
  }

  // load in the blueprint that was found, extend this class to load it
  static load(blueprintPath) {
    let Constructor;
    const constructorPath = path.resolve(blueprintPath, 'index.js');

    if (fs.lstatSync(blueprintPath).isDirectory()) {
      if (fileExists(constructorPath)) {
        const blueprintModule = require(constructorPath);
        Constructor = mixin(Blueprint, blueprintModule);

        return new Constructor(blueprintPath);
      }
    }
  }

  _fileMapTokens(options) {
    const standardTokens = {
      __name__: options => {
        const name = options.entity.name;
        const casing = options.settings.getSetting('fileCasing') || 'default';
        return normalizeCasing(name, casing);
      },
      __path__: options => {
        return options.originalBlueprintName;
      },
      __root__: options => {
        return options.settings.getSetting('sourceBase');
      },
      __test__: options => {
        return options.settings.getSetting('testBase');
      }
    };

    // HOOK: calling into the blueprints fileMapTokens() hook, passing along
    // an options hash coming from _generateFileMapVariables()
    const customTokens = this.fileMapTokens(options) || {};
    return Object.assign({}, standardTokens, customTokens);
  }

  generateFileMap(fileMapOptions) {
    const tokens = this._fileMapTokens(fileMapOptions);
    const fileMapValues = _.values(tokens);
    const tokenValues = fileMapValues.map(token => token(fileMapOptions));
    const tokenKeys = _.keys(tokens);
    return _.zipObject(tokenKeys, tokenValues);
  }

  // Set up options to be passed to fileMapTokens that get generated.
  _generateFileMapVariables(entityName, locals, options) {
    const originalBlueprintName = options.originalBlueprintName || this.name;
    const { settings, entity } = options;

    return {
      originalBlueprintName,
      settings,
      entity,
      locals
    };
  }

  // Given a file and a fileMap from locals, convert path names
  // to be correct string
  mapFile(file, locals) {
    let pattern, i;
    const fileMap = locals.fileMap || { __name__: locals.camelEntityName };
    for (i in fileMap) {
      pattern = new RegExp(i, 'g');
      file = file.replace(pattern, fileMap[i]);
    }
    return file;
  }

  _locals(options) {
    const entityName = options.entity && options.entity.name;
    const customLocals = this.locals(options);
    const fileMapVariables = this._generateFileMapVariables(
      entityName,
      customLocals,
      options
    );
    const fileMap = this.generateFileMap(fileMapVariables);

    const standardLocals = {
      pascalEntityName: normalizeCasing(entityName, 'pascal'),
      camelEntityName: normalizeCasing(entityName, 'camel'),
      snakeEntityName: normalizeCasing(entityName, 'snake'),
      dashesEntityName: normalizeCasing(entityName, 'dashes'),
      fileMap
    };

    return Object.assign({}, standardLocals, customLocals);
  }

  _process(options, beforeHook, process, afterHook) {
    const locals = this._locals(options);
    return Promise.resolve()
      .then(beforeHook.bind(this, options, locals))
      .then(process.bind(this, locals))
      .then(afterHook.bind(this, options));
  }

  processFiles(locals) {
    const files = this.files();
    const fileInfos = files.map(file => this.buildFileInfo(locals, file));
    this.ui.writeDebug(`built file infos: ${fileInfos.length}`);
    const filesToWrite = fileInfos.filter(info => info.isFile());
    this.ui.writeDebug(`files to write: ${filesToWrite.length}`);
    filesToWrite.map(file => file.writeFile(this.dryRun));
  }

  buildFileInfo(locals, file) {
    const mappedPath = this.mapFile(file, locals);
    this.ui.writeDebug(`mapped path: ${mappedPath}`);

    return new FileInfo({
      ui: this.ui,
      templateVariables: locals,
      originalPath: this.srcPath(file),
      mappedPath: this.destPath(mappedPath),
      outputPath: this.destPath(file)
    });
  }

  // where the files will be written to
  destPath(mappedPath) {
    return path.resolve(basePath, mappedPath);
  }

  // location of the string templates
  srcPath(file) {
    return path.resolve(this.filesPath(), file);
  }

  /*
   * install options:

    const blueprintOptions = {
      originalBlueprintName: name,
      ui: this.ui,
      settings: this.settings,
      entity: {
        name: cliArgs.entity.name,
        options cliArgs.entity.options
      }
    };
  */
  install(options) {
    const ui = (this.ui = options.ui);
    this.dryRun = options.dryRun;

    ui.writeInfo('installing blueprint...');
    return this._process(
      options,
      this.beforeInstall,
      this.processFiles,
      this.afterInstall
    ).then(() => {
      ui.writeInfo('finished installing blueprint.');
    });
  }

  // uninstall() {
  // }

  // HOOKS:
  locals() {}
  fileMapTokens() {}
  beforeInstall() {}
  afterInstall() {}

  // TODO: add uninstall hooks once support for uninstall exists
  // beforeUninstall() {}
  // afterUninstall() {}

  // HOOK: for normalizing entity name that gets passed in as an arg
  // via the CLI
  // normalizeEntityName(options) {
  //   return normalizeEntityName(name);
  // }
}