src/commands/init/generateTemplate/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
import path from 'path';

import Metalsmith from 'metalsmith';
import Handlebars from 'handlebars';
import async from 'async';

import log from '../../../log/default/small';

import getOptions from './options';
import ask from './ask';
import collect from './collect';
import filter from './filter';

// Register default handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
    return a === b
        ? opts.fn(this)
        : opts.inverse(this);
});

Handlebars.registerHelper('unless_eq', function (a, b, opts) {
    return a === b
        ? opts.inverse(this)
        : opts.fn(this);
});

export default function generateTemplate(name, src, dest, done) {
    const opts = getOptions(name, src);
    const metalsmith = new Metalsmith(path.join(src, 'template'));
    metalsmith.metadata({
        destDirName: path.relative(process.cwd(), dest),
        inPlace: dest === process.cwd(),
    });

    if (opts.helpers) {
        Object.keys(opts.helpers).forEach((key) => {
            Handlebars.registerHelper(key, opts.helpers[key]);
        });
    }

    metalsmith
        .use(askQuestions(opts.prompts))
        .use(collectData(opts.data))
        .use(filterFiles(opts.filters))
        .use(renderTemplateFiles)
        .clean(false)
        .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
        .destination(dest)
        .build((error) => {
            if (error) {
                log.error('An error happened when creating the template', error);
            }
            log.success('Project created');
            done();
            logMessage(opts.completionMessage, metalsmith.metadata());
        });
}

function askQuestions(prompts) {
    return (files, metalsmith, done) => ask(prompts, metalsmith.metadata(), done);
}

function collectData(data) {
    return (files, metalsmith, done) => collect(data, metalsmith.metadata(), done);
}

function filterFiles(filters) {
    return (files, metalsmith, done) => filter(files, filters, metalsmith.metadata(), done);
}

function render(template, data, callback) {
    let rendered;
    try {
        rendered = Handlebars.compile(template, { noEscape: true })(data);
    } catch (e) {
        return callback(e);
    }
    return callback(null, rendered);
}

function renderTemplateFiles(files, metalsmith, done) {
    const filenames = Object.keys(files);
    const metalsmithMetadata = metalsmith.metadata();
    async.each(filenames, (filename, next) => {  // eslint-disable-line
        // Rename files that that might use mustaches
        render(filename, metalsmithMetadata, (renameError, newFilename) => {
            if (renameError) return next(renameError);

            // If we got a new filename we replace the old file with the new
            if (newFilename !== filename) {
                files[newFilename] = files[filename]; // eslint-disable-line
                delete files[filename]; // eslint-disable-line
            }

            const str = files[newFilename].contents.toString();
            // do not attempt to render files that do not have mustaches
            if (!/{{([^{}]+)}}/g.test(str)) {
                return next();
            }

            render(str, metalsmithMetadata, (contentError, res) => {
                if (contentError) return next(contentError);

                files[newFilename].contents = new Buffer(res); // eslint-disable-line
                return next();
            });
        });
    }, done);
}

function logMessage(message, data) {
    if (!message) {
        return;
    }

    render(message, data, (err, res) => {
        if (err) {
            log.warn('\n   Error when rendering template complete message: ' + err.message.trim());
        } else {
            console.log('\n' + res.split(/\r?\n/g).map((line) => '  ' + line).join('\n'));
        }
    });
}