Skip to content

Commit

Permalink
feat: create interactive dialogue to create new app
Browse files Browse the repository at this point in the history
Create an interactive dialogue where the user can select a number of
options for the app or lab that he/she wants to create.

closes #15
  • Loading branch information
juancarlosfarah committed Feb 8, 2019
1 parent 7280841 commit bf17e93
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 39 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"bin": {
"graasp": "./lib/index.js"
},
"yargs": {
"boolean-negation": false
},
"homepage": "https://github.com/graasp/graasp-cli#readme",
"devDependencies": {
"@babel/cli": "7.2.3",
Expand All @@ -52,10 +55,12 @@
},
"dependencies": {
"@babel/polyfill": "7.2.5",
"bson-objectid": "1.2.4",
"execa": "1.0.0",
"fs-exists-cached": "1.0.0",
"fs-extra": "7.0.1",
"hosted-git-info": "2.7.1",
"inquirer": "6.2.2",
"yargs": "12.0.5"
}
}
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export const DEFAULT_STARTER = 'graasp/graasp-app-starter-react';
export const DEFAULT_FRAMEWORK = 'react';
export const DEFAULT_PATH = './';

// environments
export const LOCAL = 'local';
export const DEV = 'dev';
export const PROD = 'prod';

// keys
export const AWS_ACCESS_KEY_ID_LENGTH = 20;
export const AWS_SECRET_ACCESS_KEY_LENGTH = 40;
23 changes: 15 additions & 8 deletions src/createCli.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import yargs from 'yargs';
import initStarter from './initStarter';
import prompt from './prompt';
import { DEFAULT_STARTER } from './config';

const promisify = fn => (...args) => {
Expand Down Expand Up @@ -31,20 +31,27 @@ const createCli = (argv) => {

return cli
.command({
command: 'new [projectDirectory]',
command: 'new',
desc: 'Create new Graasp app.',
builder: _ => _.option('s', {
alias: 'starter',
type: 'string',
default: DEFAULT_STARTER,
describe: `Set starter. Defaults to ${DEFAULT_STARTER}`,
}).option('f', {
alias: 'framework',
type: 'string',
describe: 'Set development framework (e.g. React, Angular)',
}).option('t', {
alias: 'type',
choices: ['app', 'lab'],
describe: 'Type of application (app or lab)',
}).option('p', {
alias: 'path',
type: 'string',
describe: 'Path where project directory will be set up.',
}),
handler: promisify(
({
starter,
projectDirectory,
}) => initStarter(projectDirectory, { starter }),
),
handler: promisify(prompt),
})
.wrap(cli.terminalWidth())
.demandCommand(1, 'Pass --help to see all available commands and options.')
Expand Down
61 changes: 40 additions & 21 deletions src/initStarter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import execa from 'execa';
import fs from 'fs-extra';
import HostedGitInfo from 'hosted-git-info';
import { sync as existsSync } from 'fs-exists-cached';
import { DEFAULT_STARTER } from './config';
import { DEFAULT_PATH, DEFAULT_STARTER } from './config';
import writeEnvFiles from './writeEnvFiles';

// use execa to spawn a better child process
Expand Down Expand Up @@ -108,35 +108,54 @@ const clone = async (hostInfo, rootPath) => {
console.log('created starter directory layout');

await fs.remove(path.join(rootPath, '.git'));

await initGit(rootPath);

console.log('initialized git repository');

await install(rootPath);

await writeEnvFiles(rootPath);

await commit(rootPath);
};

const initStarter = async (projectDirectory, options = {}) => {
const rootPath = projectDirectory || process.cwd();

const { starter = DEFAULT_STARTER } = options;

if (existsSync(path.join(rootPath, 'package.json'))) {
console.error(`destination path '${rootPath}' is already an npm project`);
const initStarter = async (options = {}) => {
const {
starter = DEFAULT_STARTER,
name,
type,
graaspDeveloperId,
graaspAppId,
awsAccessKeyId,
awsSecretAccessKey,
p = DEFAULT_PATH,
} = options;

// enforce naming convention
const projectDirectory = path.join(p, `graasp-${type}-${name.split(' ').join('-')}`.toLowerCase());

// check for existing project in project directory
if (existsSync(path.join(projectDirectory, 'package.json'))) {
console.error(`destination path '${projectDirectory}' is already an npm project`);
return false;
}

if (existsSync(path.join(rootPath, '.git'))) {
console.error(`destination path '${rootPath}' is already a git repository`);
// check for existing git repo in project directory
if (existsSync(path.join(projectDirectory, '.git'))) {
console.error(`destination path '${projectDirectory}' is already a git repository`);
return false;
}

// clone starter kit to project directory
const hostedInfo = HostedGitInfo.fromUrl(starter);
return clone(hostedInfo, rootPath);
await clone(hostedInfo, projectDirectory);

await initGit(projectDirectory);

console.log('initialized git repository');

await install(projectDirectory);

// write environment files
await writeEnvFiles(projectDirectory, {
graaspDeveloperId,
graaspAppId,
awsAccessKeyId,
awsSecretAccessKey,
});

return commit(projectDirectory);
};

export default initStarter;
135 changes: 135 additions & 0 deletions src/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import inquirer from 'inquirer';
import ObjectId from 'bson-objectid';
import {
AWS_ACCESS_KEY_ID_LENGTH,
AWS_SECRET_ACCESS_KEY_LENGTH,
} from './config';
import initStarter from './initStarter';

const validateGraaspDeveloperId = (value) => {
// allow valid object ids or empty
if (ObjectId.isValid(value) || value === '') {
return true;
}
return 'Graasp Developer ID is not valid. Leave it empty if you do not have it yet.';
};

const validateGraaspAppId = (value) => {
// allow valid object ids
if (ObjectId.isValid(value) || value === '') {
return true;
}
return 'Graasp App ID is not valid. Leave it empty so that we automatically generate one for you.';
};

const validateAwsAccessKeyId = (value) => {
// allow valid length or empty strings
if (value.length === AWS_ACCESS_KEY_ID_LENGTH || value === '') {
return true;
}
return 'AWS Access Key ID is not valid. Leave it empty if you do not have it yet.';
};

const validateAwsSecretAccessKey = (value) => {
// allow valid length or empty strings
if (value.length === AWS_SECRET_ACCESS_KEY_LENGTH || value === '') {
return true;
}
return 'AWS Secret Access Key is not valid. Leave it empty if you do not have it yet.';
};

const prompt = async (opts) => {
const { type } = opts;

const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Name',
default: 'My App',
},
{
type: 'list',
message: 'Type',
name: 'type',
choices: [
{
name: 'App',
checked: true,
},
{
name: 'Lab',
},
],
filter: val => val.toLowerCase(),
when: () => Boolean(!type),
},
{
type: 'list',
message: 'Framework',
name: 'framework',
choices: [
{
name: 'React',
checked: true,
},
],
filter: val => val.toLowerCase(),
},
{
type: 'confirm',
name: 'api',
message: 'Use Graasp API',
default: true,
},
{
type: 'confirm',
name: 'ecosystem',
message: 'Deploy to Graasp Ecosystem',
default: true,
},
{
type: 'input',
name: 'graaspDeveloperId',
message: 'Graasp Developer ID',
when: responses => Boolean(responses.ecosystem),
validate: validateGraaspDeveloperId,
},
{
type: 'input',
name: 'graaspAppId',
message: 'Graasp App ID',
default: () => ObjectId().str,
when: responses => Boolean(responses.ecosystem),
validate: validateGraaspAppId,
},
{
type: 'input',
name: 'awsAccessKeyId',
message: 'AWS Access Key ID',
when: responses => Boolean(responses.ecosystem),
validate: validateAwsAccessKeyId,
},
{
type: 'password',
name: 'awsSecretAccessKey',
message: 'AWS Secret Access Key',
mask: '*',
when: responses => Boolean(responses.ecosystem),
validate: validateAwsSecretAccessKey,
},
]);

// default to random graasp app id
if (answers.graaspAppId === '') {
answers.graaspAppId = ObjectId().str;
}

const config = {
...answers,
...opts,
};
return initStarter(config);
};

export default prompt;
14 changes: 6 additions & 8 deletions src/writeEnvFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ const writeRemoteEnvFile = async (
) => {
const host = env === PROD ? 'apps.graasp.eu' : 'apps.dev.graasp.eu';
const bucket = `graasp-apps-${env}`;
const string = `
REACT_APP_GRAASP_DEVELOPER_ID=${graaspDeveloperId}
const string = `REACT_APP_GRAASP_DEVELOPER_ID=${graaspDeveloperId}
REACT_APP_GRAASP_APP_ID=${graaspAppId}
REACT_APP_GRAASP_DOMAIN=graasp.eu
REACT_APP_HOST=${host}
Expand All @@ -35,8 +34,7 @@ const writeRemoteEnvFile = async (
};

const writeLocalEnvFile = async (env, rootPath) => {
const string = `
REACT_APP_GRAASP_DEVELOPER_ID=
const string = `REACT_APP_GRAASP_DEVELOPER_ID=
REACT_APP_GRAASP_APP_ID=
REACT_APP_GRAASP_DOMAIN=localhost
REACT_APP_HOST=
Expand All @@ -51,22 +49,22 @@ const writeLocalEnvFile = async (env, rootPath) => {
};


const writeEnvFile = async (env, rootPath) => {
const writeEnvFile = async (env, rootPath, opts) => {
switch (env) {
case LOCAL:
return writeLocalEnvFile(env, rootPath);
case DEV:
case PROD:
return writeRemoteEnvFile(env, rootPath);
return writeRemoteEnvFile(env, rootPath, opts);
default:
return false;
}
};


const writeEnvFiles = async (rootPath) => {
const writeEnvFiles = async (rootPath, opts) => {
console.log('writing environment files...');
await Promise.all([LOCAL, DEV, PROD].map(env => writeEnvFile(env, rootPath)));
await Promise.all([LOCAL, DEV, PROD].map(env => writeEnvFile(env, rootPath, opts)));
console.log('wrote environment files');
};

Expand Down
Loading

0 comments on commit bf17e93

Please sign in to comment.