src/commands/init/fetchTemplate.js

Summary

Maintainability
C
1 day
Test Coverage
import path from 'path';
import fs from 'fs';

import { removeSync } from 'fs-extra';
import chalk from 'chalk';
import downloadGitRepo from 'download-git-repo';
import extract from 'extract-zip';
import gitClone from 'git-clone';
import ora from 'ora';
import temp from 'temp';

import fileExists from '../../helpers/fileExists';
import folderExists from '../../helpers/folderExists';
import getAbsolutePath from '../../helpers/getAbsolutePath';
import log from '../../log/default/small';

import { getVersions, getOfficialTemplates } from './githubHelpers';

// Automatically track and cleanup files at exit
temp.track();

const defaultVersion = 'next'; // replace with "latest" when we release as latest on npm - temporary

export function isLocal(template, directory) {
    return folderExists(template, directory) ||
        (path.extname(template) === '.zip' && fileExists(template, directory));
}

export default function fetchTemplate(template, directory, version, { clone }) {
    // The provided template is a local folder
    if (folderExists(template, directory)) {
        return getAbsolutePath(template, directory);
    }

    // The provided template is a zip file
    if (path.extname(template) === '.zip' && fileExists(template, directory)) {
        return unzip(getAbsolutePath(template, directory));
    }

    // Handle if official/listed template
    if (template.indexOf('/') === -1) {
        return getOfficialTemplate(template, version);
    }

    // A git clone url
    if (template.startsWith('git@')) {
        return cloneRepo(template, version);
    }

    // Handle GitHub templates in first class way, for better support of versions
    if (!clone && isGitHub(template)) {
        return github(template, version);
    }

    // Otherwise try to use download-git-repo
    return download(template, version, clone);
}

function unzip(template) {
    const tmp = temp.mkdirSync('roc-template');

    return new Promise((resolve) => {
        extract(template, { dir: tmp }, (error) => {
            if (error) {
                log.error(`Failed to extract template from "${template}"`, error);
            }
            const files = fs.readdirSync(tmp);
            if (files.length !== 1) {
                log.error(`The template seems no to be structured correctly "${template}"`);
            }

            resolve(path.join(tmp, files[0]));
        });
    });
}

async function getOfficialTemplate(template, version) {
    try {
        const templates = await getOfficialTemplates();

        // Match on the fullname and the short name
        const selectedTemplate = templates.find(({ name }) => name === template || name === `roc-template-${template}`);

        if (!selectedTemplate) {
            log.error(`Could not find a template matching ${chalk.bold(template)}, ` +
                'run command without arguments to list the official ones');
        }

        return github(selectedTemplate.full_name, version);
    } catch (error) {
        log.error('Failed to fetch the list of official templates', error);
    }
}

async function github(template, version) {
    // Temporary to get next for the moment
    if (!version) {
        version = defaultVersion; // eslint-disable-line
        log.info(`Will use ${chalk.bold(version)} as default version`);
    }

    // Get the versions for the template if no version is provided or it starts with a number
    if (!version || !isNaN(Number(version.charAt(0)))) {
        try {
            const versions = await getVersions(template);

            // Add master so we always have a way to install it
            versions.push({ name: 'master' });

            // Add v to numeric versions
            const selectVersion = (
                version &&
                !isNaN(Number(version.charAt(0))) &&
                version.charAt(0) !== 'v'
            ) ? `v${version}` : version;

            const selectedVersion = versions.find((v) => v.name === selectVersion);
            // eslint-disable-next-line
            version = (selectedVersion && selectedVersion.name) || // Try to get the requested version
                (versions[0] && versions[0].name) || // Otherwise take the latest version
                defaultVersion; // Fallback to use latest

            if (!selectedVersion && selectVersion) {
                log.warn(`Selected template version not found, using ${chalk.bold(version)}`);
            } else if (!selectedVersion) {
                log.info(`Using ${chalk.bold(version)} as template version`);
            }
        } catch (error) {
            version = defaultVersion; // eslint-disable-line
            log.info(`Failed to fetch versions, will fallback to ${chalk.bold(version)}`, error);
        }
    }

    return download(template, version);
}

function download(template, version, clone = false) {
    if (!version) {
        version = defaultVersion; // eslint-disable-line
        log.info(`Will use ${chalk.bold(version)} as default version`);
    }

    const tmp = temp.mkdirSync('roc-template');

    const spinner = ora('Downloading template').start();

    return new Promise((resolve) => {
        try {
            downloadGitRepo(`${template}${version && '#' + encodeURIComponent(version)}`, tmp, { clone }, (error) => {
                if (error) {
                    spinner.fail();
                    log.error(
                        `Failed to download the template from ${chalk.bold(template)} using ${chalk.bold(version)}`,
                        error
                    );
                }

                spinner.succeed();
                resolve(tmp);
            });
        } catch (error) {
            log.error('An error happened when downloading the template.', error);
        }
    });
}

function cloneRepo(template, version) {
    if (!version) {
        version = defaultVersion; // eslint-disable-line
        log.info(`Will use ${chalk.bold(version)} as default version`);
    }

    const tmp = temp.mkdirSync('roc-template');

    const spinner = ora('Downloading template').start();
    return new Promise((resolve) => {
        try {
            gitClone(template, tmp, { checkout: version }, (error) => {
                if (error) {
                    spinner.fail();
                    log.error(
                        `Failed to download the template from ${chalk.bold(template)} using ${chalk.bold(version)}`,
                        error
                    );
                }

                spinner.succeed();

                removeSync(path.join(tmp, '.git'));
                resolve(tmp);
            });
        } catch (error) {
            log.error('An error happened when downloading the template.', error);
        }
    });
}


export function isGitHub(template) {
    return template.indexOf(':') === -1 && (template.match(/\//g) || []).length === 1;
}