Skip to content

Commit

Permalink
Merge pull request #109 from rocjs/template-as-a-zip
Browse files Browse the repository at this point in the history
Template as a zip
  • Loading branch information
dlmr committed May 20, 2016
2 parents 45f76a7 + 481fbd1 commit 28189be
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
lib
esdocs
coverage
.idea
4 changes: 4 additions & 0 deletions docs/config/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ A string command is a string that will managed as if it was typed into the termi
The function will be invoked with an object with the following properties.
```
verbose If verbose mode has been enabled
directory Path to working directory
info The object that is passed to the runCli function with version and name
configObject The final configuration object where everything has been merged
metaObject The final meta configuration object where everything has been merged
Expand All @@ -42,6 +43,9 @@ actions The currently registered actions
### verbose
Debug will be set to `true` if `-V, --verbose` was set. Should be used to print extra information when running the command. Otherwise it will be `false`.

### directory
If set it will be used as the current working directory. The path can be either relative to the current working directory or absolute.

### info
The same information object as `runCli` was invoked with, meaning it should have two properties.
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"tar": "~2.2.1",
"temp": "~0.8.3",
"trim-newlines": "~1.0.0",
"unzip": "^0.1.11",
"update-notifier": "0.6.3"
}
}
1 change: 1 addition & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function runCli(info = { version: 'Unknown', name: 'Unknown' }, initalCon
// Run the command
return configObject.commands[command]({
verbose: verboseMode,
directory: dirPath,
info,
configObject,
metaObject,
Expand Down
47 changes: 47 additions & 0 deletions src/commands/helpers/unzip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import temp from 'temp';
import fs from 'fs';
import nodeUnzip from 'unzip';
import path from 'path';

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

/**
* Unpacks the given zip file.
*
* @param {string} zipFile - The full path to a local zip file.
*
* @returns {Promise} The path to the temporary directory where the unzipped files are located.
*/
export default function unzip(zipFile) {
if (!zipFile) {
throw new Error('No zip file was given.');
}

return new Promise((resolve, reject) => {
temp.mkdir('roc', (err, dirPath) => {
if (err) {
return reject(err);
}

fs.createReadStream(zipFile)
.pipe(nodeUnzip.Extract({ path: dirPath })) // eslint-disable-line new-cap
.on('error', reject)
.on('close', () => {
// The template zip is assumed to have a root dir
try {
fs.readdirSync(dirPath)
.forEach((file) => {
const rootDir = path.join(dirPath, file);
if (fs.statSync(rootDir).isDirectory()) {
return resolve(rootDir);
}
});
} catch (error) {
return reject(error);
}
return resolve(dirPath);
});
});
});
}
48 changes: 29 additions & 19 deletions src/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import chalk from 'chalk';
import { get, getVersions } from './helpers/github';
import { validRocProject } from './helpers/general';
import { error, warning, info, ok } from '../helpers/style';
import { getAbsolutePath } from '../helpers';
import unzip from './helpers/unzip';

/* This should be fetched from a server!
*/
Expand All @@ -30,7 +32,7 @@ const templates = [{
*
* @returns {Promise} - Promise for the command.
*/
export default function init({ parsedArguments, parsedOptions }) {
export default function init({ parsedArguments, parsedOptions, directory }) {
const { list, force } = parsedOptions.options;
const { name, template, version } = parsedArguments.arguments;

Expand All @@ -42,19 +44,23 @@ export default function init({ parsedArguments, parsedOptions }) {
}

// Make sure the directory is empty!
return checkFolder(force, name).then((directory) => {
return checkFolder(force, name, directory).then((dir) => {
if (!template) {
return interactiveMenu(directory, list);
return interactiveMenu(dir, list);
}

return fetchTemplate(template, version, directory, list);
return fetchTemplate(template, version, dir, list);
});
}

/*
* Helpers
*/

function isLocalTemplate(template = '') {
return template.indexOf('.zip') === template.length - 4;
}

function getTemplateVersion(toFetch, list) {
return getVersions(toFetch)
.then((versions) => {
Expand All @@ -74,7 +80,9 @@ function getTemplateVersion(toFetch, list) {
}

function getTemplate(template) {
if (template.indexOf('/') === -1) {
if (isLocalTemplate(template)) {
return template;
} else if (template.indexOf('/') === -1) {
const selectedTemplate = templates.find((elem) => elem.identifier === template);
if (!selectedTemplate) {
console.log(error('Invalid template name given.'));
Expand All @@ -92,8 +100,9 @@ function getTemplate(template) {
function fetchTemplate(toFetch, selectVersion, directory, list) {
toFetch = getTemplate(toFetch);

return getTemplateVersion(toFetch, list)
.then((versions) => {
const templateFetcher = isLocalTemplate(toFetch) ?
unzip(toFetch) :
getTemplateVersion(toFetch, list).then((versions) => {
// If the name starts with a number we will automatically add 'v' infront of it to match Github default
if (selectVersion && !isNaN(Number(selectVersion.charAt(0))) && selectVersion.charAt(0) !== 'v') {
selectVersion = `v${selectVersion}`;
Expand All @@ -113,7 +122,9 @@ function fetchTemplate(toFetch, selectVersion, directory, list) {
}

return get(toFetch, actualVersion);
})
});

return templateFetcher
.then((dirPath) => {
if (!validRocProject(path.join(dirPath, 'template'))) {
/* eslint-disable no-process-exit */
Expand Down Expand Up @@ -248,22 +259,21 @@ function interactiveMenu(directory, list) {
});
}

function checkFolder(force = false, directoryName = '') {
function checkFolder(force = false, directoryName = '', directory = '') {
return new Promise((resolve) => {
const directoryPath = path.join(process.cwd(), directoryName);
const directoryPath = getAbsolutePath(path.join(directory, directoryName));
fs.mkdir(directoryPath, (err) => {
if (directoryName && err) {
if (err) {
console.log(
warning(`Found a folder named ${chalk.underline(directoryName)} at ` +
`${chalk.underline(process.cwd())}, will try to use it.`)
, '\n');
warning(`Found a folder named ${chalk.underline(directoryPath)}, will try to use it.`)
, '\n');
}

if (!force && fs.readdirSync(directoryPath).length > 0) {
inquirer.prompt([{
type: 'list',
name: 'selection',
message: 'The directory is not empty, what do you want to do?',
message: `The directory '${directoryPath}' is not empty, what do you want to do?`,
choices: [{
name: 'Create new folder',
value: 'new'
Expand All @@ -280,7 +290,7 @@ function checkFolder(force = false, directoryName = '') {
process.exit(1);
/* eslint-enable */
} else if (selection === 'new') {
askForDirectory(resolve);
askForDirectory(directory, resolve);
} else if (selection === 'force') {
resolve(directoryPath);
}
Expand All @@ -292,13 +302,13 @@ function checkFolder(force = false, directoryName = '') {
});
}

function askForDirectory(resolve) {
function askForDirectory(directory, resolve) {
inquirer.prompt([{
type: 'input',
name: 'name',
message: 'What do you want to name the directory?'
message: `What do you want to name the directory? (It will be created in '${directory || process.cwd()}')`
}], ({ name }) => {
const directoryPath = path.join(process.cwd(), name);
const directoryPath = getAbsolutePath(name, directory);
fs.mkdir(directoryPath, (err) => {
if (err) {
console.log(warning('The directory did already exists or was not empty.'), '\n');
Expand Down
4 changes: 3 additions & 1 deletion src/roc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const initOptions = [{
const initArguments = [{
name: 'template',
validation: isPath,
description: 'The template to use. Matches Github structure with Username/Repo.'
description: 'The template to use. Matches Github structure with Username/Repo or a local zip file.'
}, {
name: 'version',
validation: isString,
Expand Down Expand Up @@ -64,6 +64,8 @@ const roc = {
__template__
Template can either be a short name for a specific template, currently it accepts \`web-app\` and \`web-app-react\` that will be converted internally to \`rocjs/roc-template-web-app\` and \`rocjs/roc-template-web-app-react\`. As can be seen here the actual template reference is a Github repo and can be anything matching that pattern \`USERNAME/PROJECT\`.
The template can also point to a local zip file (ending in \`.zip\`) of a template repository. This is useful if the template is on a private repo or not on GitHub.
It will also expect that the template has a folder named \`template\` and that inside of it there is \`package.json\` file with at least one dependency to a Roc module following the pattern \`roc-package-*\` or that it has a \`roc.config.js\` file (this file is then expected to have some [packages](/docs/config/packages.md) defined but this is not checked immediately).
__version__
Expand Down
3 changes: 3 additions & 0 deletions test/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('roc', () => {

expect(spy.calls[0].arguments[0]).toEqual({
verbose: false,
directory: undefined,
info: info,
configObject: config,
packageConfig: config,
Expand All @@ -100,6 +101,7 @@ describe('roc', () => {

expect(spy.calls[0].arguments[0]).toEqual({
verbose: true,
directory: undefined,
info: info,
configObject: config,
packageConfig: config,
Expand Down Expand Up @@ -130,6 +132,7 @@ describe('roc', () => {

expect(spy.calls[0].arguments[0]).toEqual({
verbose: false,
directory: undefined,
info: info,
configObject: newConfig,
packageConfig: config,
Expand Down
65 changes: 49 additions & 16 deletions test/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ describe('roc', () => {
after(() => {
});

function setupMocks({ versions = [{ name: 'v1.0' }] } = {}) {
const dirPath = path.join(__dirname, 'data', 'valid-template');

prompt.andCall((options, cb) => {
cb({option: 'web'});
});
readdirSync.andReturn([]);
getVersions.andReturn(Promise.resolve(versions));
get.andReturn(Promise.resolve(dirPath));

return dirPath;
}

it('should give information about non empty directory and terminate process if selected', () => {
readdirSync.andReturn([1]);

Expand All @@ -80,7 +93,7 @@ describe('roc', () => {
return init({ parsedArguments: { arguments: {} }, parsedOptions: { options: {} } })
.catch((error) => {
expect(prompt.calls[0].arguments[0][0].message)
.toBe('The directory is not empty, what do you want to do?');
.toBe(`The directory '${process.cwd()}' is not empty, what do you want to do?`);
expect(error).toBeA(Error);
});
});
Expand All @@ -97,7 +110,8 @@ describe('roc', () => {
return init({ parsedArguments: { arguments: {} }, parsedOptions: { options: {} } })
.catch(() => {
expect(prompt.calls[1].arguments[0][0].message)
.toBe('What do you want to name the directory?');
.toBe('What do you want to name the directory? ' +
`(It will be created in '${process.cwd()}')`);
});
});
});
Expand Down Expand Up @@ -133,14 +147,8 @@ describe('roc', () => {
});

it('should directly fetch template if provided by full name without version given', () => {
const dirPath = path.join(__dirname, 'data', 'valid-template');
const dirPath = setupMocks({ versions: ['1.0'] });

prompt.andCall((options, cb) => {
cb({option: 'web'});
});
readdirSync.andReturn([]);
getVersions.andReturn(Promise.resolve(['1.0']));
get.andReturn(Promise.resolve(dirPath));
return consoleMockWrapper((log) => {
return init({
parsedArguments: { arguments: { template: 'vgno/roc-template-web' } },
Expand All @@ -162,6 +170,37 @@ describe('roc', () => {
});
});

it('should use directory as install dir', () => {
const dirPath = setupMocks();

return consoleMockWrapper(() => {
return init({
directory: 'roc-directory',
parsedArguments: { arguments: { template: 'vgno/roc-template-web' } },
parsedOptions: { options: {} }
}).then(() => {
expect(spawn.calls[0].arguments[2].cwd).toEqual(dirPath);
expect(spawn.calls[1].arguments[2].cwd).toEqual(path.join(process.cwd(), 'roc-directory'));
});
});
});

it('should choose name over directory', () => {
const dirPath = setupMocks();

return consoleMockWrapper(() => {
return init({
directory: 'roc-directory',
parsedArguments: { arguments: { template: 'vgno/roc-template-web', name: 'roc-name' } },
parsedOptions: { options: {} }
}).then(() => {
expect(spawn.calls[0].arguments[2].cwd).toEqual(dirPath);
expect(spawn.calls[1].arguments[2].cwd)
.toEqual(path.join(process.cwd(), 'roc-directory', 'roc-name'));
});
});
});

it('should list versions if asked', () => {
readdirSync.andReturn([]);
getVersions.andReturn(Promise.resolve([{name: '1.0'}]));
Expand Down Expand Up @@ -203,14 +242,8 @@ describe('roc', () => {
});

it('should manage the provided template version as expected', () => {
const dirPath = path.join(__dirname, 'data', 'valid-template');
const dirPath = setupMocks({ versions: [{ name: 'v1.0' }] });

prompt.andCall((options, cb) => {
cb({option: 'web'});
});
readdirSync.andReturn([]);
getVersions.andReturn(Promise.resolve([{name: 'v1.0'}]));
get.andReturn(Promise.resolve(path.join(__dirname, 'data', 'valid-template')));
return consoleMockWrapper((log) => {
return init({
parsedArguments: { arguments:
Expand Down

0 comments on commit 28189be

Please sign in to comment.