Skip to content

Commit bf17e93

Browse files
feat: create interactive dialogue to create new app
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
1 parent 7280841 commit bf17e93

7 files changed

Lines changed: 245 additions & 39 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
"bin": {
3636
"graasp": "./lib/index.js"
3737
},
38+
"yargs": {
39+
"boolean-negation": false
40+
},
3841
"homepage": "https://github.com/graasp/graasp-cli#readme",
3942
"devDependencies": {
4043
"@babel/cli": "7.2.3",
@@ -52,10 +55,12 @@
5255
},
5356
"dependencies": {
5457
"@babel/polyfill": "7.2.5",
58+
"bson-objectid": "1.2.4",
5559
"execa": "1.0.0",
5660
"fs-exists-cached": "1.0.0",
5761
"fs-extra": "7.0.1",
5862
"hosted-git-info": "2.7.1",
63+
"inquirer": "6.2.2",
5964
"yargs": "12.0.5"
6065
}
6166
}

src/config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
export const DEFAULT_STARTER = 'graasp/graasp-app-starter-react';
2+
export const DEFAULT_FRAMEWORK = 'react';
3+
export const DEFAULT_PATH = './';
24

35
// environments
46
export const LOCAL = 'local';
57
export const DEV = 'dev';
68
export const PROD = 'prod';
9+
10+
// keys
11+
export const AWS_ACCESS_KEY_ID_LENGTH = 20;
12+
export const AWS_SECRET_ACCESS_KEY_LENGTH = 40;

src/createCli.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import yargs from 'yargs';
2-
import initStarter from './initStarter';
2+
import prompt from './prompt';
33
import { DEFAULT_STARTER } from './config';
44

55
const promisify = fn => (...args) => {
@@ -31,20 +31,27 @@ const createCli = (argv) => {
3131

3232
return cli
3333
.command({
34-
command: 'new [projectDirectory]',
34+
command: 'new',
3535
desc: 'Create new Graasp app.',
3636
builder: _ => _.option('s', {
3737
alias: 'starter',
3838
type: 'string',
3939
default: DEFAULT_STARTER,
4040
describe: `Set starter. Defaults to ${DEFAULT_STARTER}`,
41+
}).option('f', {
42+
alias: 'framework',
43+
type: 'string',
44+
describe: 'Set development framework (e.g. React, Angular)',
45+
}).option('t', {
46+
alias: 'type',
47+
choices: ['app', 'lab'],
48+
describe: 'Type of application (app or lab)',
49+
}).option('p', {
50+
alias: 'path',
51+
type: 'string',
52+
describe: 'Path where project directory will be set up.',
4153
}),
42-
handler: promisify(
43-
({
44-
starter,
45-
projectDirectory,
46-
}) => initStarter(projectDirectory, { starter }),
47-
),
54+
handler: promisify(prompt),
4855
})
4956
.wrap(cli.terminalWidth())
5057
.demandCommand(1, 'Pass --help to see all available commands and options.')

src/initStarter.js

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import execa from 'execa';
44
import fs from 'fs-extra';
55
import HostedGitInfo from 'hosted-git-info';
66
import { sync as existsSync } from 'fs-exists-cached';
7-
import { DEFAULT_STARTER } from './config';
7+
import { DEFAULT_PATH, DEFAULT_STARTER } from './config';
88
import writeEnvFiles from './writeEnvFiles';
99

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

110110
await fs.remove(path.join(rootPath, '.git'));
111-
112-
await initGit(rootPath);
113-
114-
console.log('initialized git repository');
115-
116-
await install(rootPath);
117-
118-
await writeEnvFiles(rootPath);
119-
120-
await commit(rootPath);
121111
};
122112

123-
const initStarter = async (projectDirectory, options = {}) => {
124-
const rootPath = projectDirectory || process.cwd();
125-
126-
const { starter = DEFAULT_STARTER } = options;
127-
128-
if (existsSync(path.join(rootPath, 'package.json'))) {
129-
console.error(`destination path '${rootPath}' is already an npm project`);
113+
const initStarter = async (options = {}) => {
114+
const {
115+
starter = DEFAULT_STARTER,
116+
name,
117+
type,
118+
graaspDeveloperId,
119+
graaspAppId,
120+
awsAccessKeyId,
121+
awsSecretAccessKey,
122+
p = DEFAULT_PATH,
123+
} = options;
124+
125+
// enforce naming convention
126+
const projectDirectory = path.join(p, `graasp-${type}-${name.split(' ').join('-')}`.toLowerCase());
127+
128+
// check for existing project in project directory
129+
if (existsSync(path.join(projectDirectory, 'package.json'))) {
130+
console.error(`destination path '${projectDirectory}' is already an npm project`);
130131
return false;
131132
}
132133

133-
if (existsSync(path.join(rootPath, '.git'))) {
134-
console.error(`destination path '${rootPath}' is already a git repository`);
134+
// check for existing git repo in project directory
135+
if (existsSync(path.join(projectDirectory, '.git'))) {
136+
console.error(`destination path '${projectDirectory}' is already a git repository`);
135137
return false;
136138
}
137139

140+
// clone starter kit to project directory
138141
const hostedInfo = HostedGitInfo.fromUrl(starter);
139-
return clone(hostedInfo, rootPath);
142+
await clone(hostedInfo, projectDirectory);
143+
144+
await initGit(projectDirectory);
145+
146+
console.log('initialized git repository');
147+
148+
await install(projectDirectory);
149+
150+
// write environment files
151+
await writeEnvFiles(projectDirectory, {
152+
graaspDeveloperId,
153+
graaspAppId,
154+
awsAccessKeyId,
155+
awsSecretAccessKey,
156+
});
157+
158+
return commit(projectDirectory);
140159
};
141160

142161
export default initStarter;

src/prompt.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import inquirer from 'inquirer';
2+
import ObjectId from 'bson-objectid';
3+
import {
4+
AWS_ACCESS_KEY_ID_LENGTH,
5+
AWS_SECRET_ACCESS_KEY_LENGTH,
6+
} from './config';
7+
import initStarter from './initStarter';
8+
9+
const validateGraaspDeveloperId = (value) => {
10+
// allow valid object ids or empty
11+
if (ObjectId.isValid(value) || value === '') {
12+
return true;
13+
}
14+
return 'Graasp Developer ID is not valid. Leave it empty if you do not have it yet.';
15+
};
16+
17+
const validateGraaspAppId = (value) => {
18+
// allow valid object ids
19+
if (ObjectId.isValid(value) || value === '') {
20+
return true;
21+
}
22+
return 'Graasp App ID is not valid. Leave it empty so that we automatically generate one for you.';
23+
};
24+
25+
const validateAwsAccessKeyId = (value) => {
26+
// allow valid length or empty strings
27+
if (value.length === AWS_ACCESS_KEY_ID_LENGTH || value === '') {
28+
return true;
29+
}
30+
return 'AWS Access Key ID is not valid. Leave it empty if you do not have it yet.';
31+
};
32+
33+
const validateAwsSecretAccessKey = (value) => {
34+
// allow valid length or empty strings
35+
if (value.length === AWS_SECRET_ACCESS_KEY_LENGTH || value === '') {
36+
return true;
37+
}
38+
return 'AWS Secret Access Key is not valid. Leave it empty if you do not have it yet.';
39+
};
40+
41+
const prompt = async (opts) => {
42+
const { type } = opts;
43+
44+
const answers = await inquirer.prompt([
45+
{
46+
type: 'input',
47+
name: 'name',
48+
message: 'Name',
49+
default: 'My App',
50+
},
51+
{
52+
type: 'list',
53+
message: 'Type',
54+
name: 'type',
55+
choices: [
56+
{
57+
name: 'App',
58+
checked: true,
59+
},
60+
{
61+
name: 'Lab',
62+
},
63+
],
64+
filter: val => val.toLowerCase(),
65+
when: () => Boolean(!type),
66+
},
67+
{
68+
type: 'list',
69+
message: 'Framework',
70+
name: 'framework',
71+
choices: [
72+
{
73+
name: 'React',
74+
checked: true,
75+
},
76+
],
77+
filter: val => val.toLowerCase(),
78+
},
79+
{
80+
type: 'confirm',
81+
name: 'api',
82+
message: 'Use Graasp API',
83+
default: true,
84+
},
85+
{
86+
type: 'confirm',
87+
name: 'ecosystem',
88+
message: 'Deploy to Graasp Ecosystem',
89+
default: true,
90+
},
91+
{
92+
type: 'input',
93+
name: 'graaspDeveloperId',
94+
message: 'Graasp Developer ID',
95+
when: responses => Boolean(responses.ecosystem),
96+
validate: validateGraaspDeveloperId,
97+
},
98+
{
99+
type: 'input',
100+
name: 'graaspAppId',
101+
message: 'Graasp App ID',
102+
default: () => ObjectId().str,
103+
when: responses => Boolean(responses.ecosystem),
104+
validate: validateGraaspAppId,
105+
},
106+
{
107+
type: 'input',
108+
name: 'awsAccessKeyId',
109+
message: 'AWS Access Key ID',
110+
when: responses => Boolean(responses.ecosystem),
111+
validate: validateAwsAccessKeyId,
112+
},
113+
{
114+
type: 'password',
115+
name: 'awsSecretAccessKey',
116+
message: 'AWS Secret Access Key',
117+
mask: '*',
118+
when: responses => Boolean(responses.ecosystem),
119+
validate: validateAwsSecretAccessKey,
120+
},
121+
]);
122+
123+
// default to random graasp app id
124+
if (answers.graaspAppId === '') {
125+
answers.graaspAppId = ObjectId().str;
126+
}
127+
128+
const config = {
129+
...answers,
130+
...opts,
131+
};
132+
return initStarter(config);
133+
};
134+
135+
export default prompt;

src/writeEnvFiles.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ const writeRemoteEnvFile = async (
1414
) => {
1515
const host = env === PROD ? 'apps.graasp.eu' : 'apps.dev.graasp.eu';
1616
const bucket = `graasp-apps-${env}`;
17-
const string = `
18-
REACT_APP_GRAASP_DEVELOPER_ID=${graaspDeveloperId}
17+
const string = `REACT_APP_GRAASP_DEVELOPER_ID=${graaspDeveloperId}
1918
REACT_APP_GRAASP_APP_ID=${graaspAppId}
2019
REACT_APP_GRAASP_DOMAIN=graasp.eu
2120
REACT_APP_HOST=${host}
@@ -35,8 +34,7 @@ const writeRemoteEnvFile = async (
3534
};
3635

3736
const writeLocalEnvFile = async (env, rootPath) => {
38-
const string = `
39-
REACT_APP_GRAASP_DEVELOPER_ID=
37+
const string = `REACT_APP_GRAASP_DEVELOPER_ID=
4038
REACT_APP_GRAASP_APP_ID=
4139
REACT_APP_GRAASP_DOMAIN=localhost
4240
REACT_APP_HOST=
@@ -51,22 +49,22 @@ const writeLocalEnvFile = async (env, rootPath) => {
5149
};
5250

5351

54-
const writeEnvFile = async (env, rootPath) => {
52+
const writeEnvFile = async (env, rootPath, opts) => {
5553
switch (env) {
5654
case LOCAL:
5755
return writeLocalEnvFile(env, rootPath);
5856
case DEV:
5957
case PROD:
60-
return writeRemoteEnvFile(env, rootPath);
58+
return writeRemoteEnvFile(env, rootPath, opts);
6159
default:
6260
return false;
6361
}
6462
};
6563

6664

67-
const writeEnvFiles = async (rootPath) => {
65+
const writeEnvFiles = async (rootPath, opts) => {
6866
console.log('writing environment files...');
69-
await Promise.all([LOCAL, DEV, PROD].map(env => writeEnvFile(env, rootPath)));
67+
await Promise.all([LOCAL, DEV, PROD].map(env => writeEnvFile(env, rootPath, opts)));
7068
console.log('wrote environment files');
7169
};
7270

0 commit comments

Comments
 (0)