all files / lib/theme/ index.js

96.88% Statements 124/128
87.18% Branches 34/39
96% Functions 24/25
92.73% Lines 51/55
7 statements, 5 branches Ignored     
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192                                                                                       24× 16×                                               16× 16×                   16×                           32×   24×     24×     24×               24×                 16×         16×         16× 16×                   16×         16× 16×                          
import glob from 'glob';
import path from 'path';
import fs from 'fs-extra';
import resolve from 'resolve';
import isArray from 'lodash/isArray';
import each from 'lodash/each';
import map from 'lodash/map';
import {getYarnPackageNames} from '../json';
import Parse from '../parse';
import * as CONSTANTS from '../constants';
import Asset from './asset';
 
export default class Theme {
  constructor() {
    /**
     * The current theme name.
     * @type {string}
     */
    this.name = '';
 
    /**
     * Raw object that holds the theme config object.
     * @type {Object}
     */
    this.config;
 
    /**
     * Collection of all assets for this theme.
     * @type {Object.<string, Asset>}
     */
    this.assets = Object.create(null);
 
    /**
     * Data of asset names and paths that is exposed to templates.
     * @type {Object}
     */
    this.data = Object.create(null);
  }
 
  /**
   * Set global config delegate function.
   * @param {Object} getConfig Config object.
   */
  setGetConfig(getConfig) {
    this._getConfig = getConfig;
  }
 
  /**
   * Find all theme.yml files in node_modules.
   * @param {string} directory Directory to load package.json from.
   * @return {Array.<string>} Array of paths to theme.yml files.
   */
  _getThemesFromPackageJson(directory = '') {
    const yarnThemePackageNameRegex = /^yarn\-theme\-/;
 
    // All yarn packages that exist in our root package.json file.
    const yarnPackages = getYarnPackageNames(directory);
 
    // Return only the path to the theme.yml file.
    return yarnPackages.reduce((packageThemes, packageName) => {
      if (!yarnThemePackageNameRegex.test(packageName)) {
        return packageThemes;
      }
 
      // Use module's main field which should reference the config yaml file.
      const yamlPath = resolve.sync(packageName, {
        basedir: directory
      });
 
      packageThemes.push(yamlPath);
 
      return packageThemes;
    }, []);
  }
 
  update() {
    this.name = this._getConfig().get('theme');
 
    const configPath = this._getConfig().get('path');
 
    // Find all theme config files that might exist.
    const fileSystemYamls = glob.sync(
      configPath.themes + `/**/${CONSTANTS.YAML.THEME}`,
      { nodir: true }
    );
 
    const moduleYamls = this._getThemesFromPackageJson(
      configPath.source
    );
 
    const allYamls = [].concat(fileSystemYamls, moduleYamls);
 
    let files = allYamls.reduce((allFiles, file) => {
      let parsedFile = Parse.fromYaml(
        fs.readFileSync(file, 'utf8')
      );
 
      // If the found theme config file name matches our set theme name
      // then save it.
      if (parsedFile.name === this.name) {
        // Save the directory where the correct theme file was found.
        parsedFile.path[CONSTANTS.KEY.SOURCE] = path.dirname(file);
        allFiles.push(parsedFile);
      }
 
      return allFiles;
    }, []);
 
    Iif (files.length === 0) {
      throw new Error(`Did not find any theme named '${this.name}' installed.`);
    } else Iif (files.length !== 1) {
      throw new Error('Found multiple themes with the same name: ' +
        files.join(','));
    }
 
    let parsedFile = files[0];
 
    // Save raw theme config.
    this.config = parsedFile;
 
    // Calculate absolute path of 'paths' keys.
    each(this.config.path, (val, key) => {
      if (key !== CONSTANTS.KEY.SOURCE) {
        // The root of the theme's destination is the site's destination.
        let rootPath = key === CONSTANTS.KEY.DESTINATION ?
          configPath.destination : this.config.path.source;
 
        let keyValue = this.config.path[key];
 
        // Support converting array of values to their absolute path.
        Iif (isArray(keyValue)) {
          this.config.path[key] = keyValue.map(value => {
            return path.resolve(
              rootPath,
              value
            );
          });
        } else {
          this.config.path[key] = path.resolve(rootPath, keyValue);
        }
      }
    });
 
    this._createAssets();
  }
 
  _createAssets() {
    // Instantiate an Asset for every asset the theme configures.
    each(this.config.assets, (assetConfig, assetType) => {
      assetConfig.destination = path.resolve(
        this.config.path.destination,
        assetConfig.source
      );
 
      assetConfig.source = path.resolve(
        this.config.path.source,
        assetConfig.source
      );
 
      let asset = new Asset(assetType, assetConfig);
      this.assets[asset.id] = asset;
    });
  }
 
  async read() {
    let pathDestination = this._getConfig().get('path.destination');
 
    // Have every asset write itself to the destination folder.
    await Promise.all(
      map(this.assets, assetProcessor => {
        return assetProcessor.process(pathDestination);
      })
    );
 
    // Expose every asset's data onto the theme's data object.
    each(this.assets, assetProcessor => {
      Eif (assetProcessor.data) {
        this.data[assetProcessor.type] = assetProcessor.data.url;
      }
    });
  }
 
  async write() {
    // Have every asset write itself to the destination folder.
    await Promise.all(
      map(this.assets, assetProcessor => {
        return assetProcessor.write();
      })
    );
  }
}