UNPKG

9.5 kBJavaScriptView Raw
1var chalk = require('chalk');
2var async = require('async');
3var request = require('request');
4var _ = require('lodash');
5var fs = require('fs');
6var urljoin = require('url-join');
7var path = require('path');
8var inquirer = require('inquirer');
9var spawn = require('../lib/spawn');
10var api = require('../lib/api');
11var log = require('../lib/log');
12var debug = require('debug')('4front:cli:create-app');
13var manifest = require('../lib/manifest');
14var template = require('../lib/template');
15var helper = require('../lib/helper');
16
17require("simple-errors");
18
19module.exports = function(program, done) {
20 program = _.defaults(program || {}, {
21 baseDir: process.cwd()
22 });
23
24 log.messageBox("Create a new 4front app");
25
26 collectInput(function(err, answers) {
27 if (err) return done(err);
28
29 // Print a blank line at the end of the questionaire
30 log.blankLine();
31
32 if (answers.confirmExistingDir === false)
33 return done("Please re-run '4front create-app' from the root of the " +
34 "directory where your existing app code resides.");
35
36 var tasks = [], appDir;
37 if (answers.startingMode === 'scratch' || program.templateUrl) {
38 // Create a new directory corresponding to the app name
39 appDir = path.join(program.baseDir, answers.appName);
40 tasks.push(function(cb) {
41 fs.exists(appDir, function(exists) {
42 if (exists === true)
43 return cb("Directory " + appDir + " already exists.");
44
45 log.info("Making app directory %s", appDir);
46 fs.mkdir(appDir, cb);
47 });
48 });
49 }
50 else
51 appDir = program.baseDir;
52
53 log.debug("Setting appDir to %s", appDir);
54
55 if (answers.templateUrl === 'blank') {
56 // Make a new package.json from scratch
57 tasks.push(function(cb) {
58 createBlankStart(answers, appDir, cb);
59 });
60 }
61 else if (answers.templateUrl) {
62 tasks.push(function(cb) {
63 unpackTemplate(answers.templateUrl, appDir, cb);
64 });
65 }
66 else if (program.templateUrl) {
67 tasks.push(function(cb) {
68 unpackTemplate(program.templateUrl, appDir, cb);
69 });
70 }
71
72 tasks.push(function(cb) {
73 npmInstall(appDir, cb);
74 });
75
76 tasks.push(function(cb) {
77 bowerInstall(appDir, cb);
78 });
79
80 var createdApp = null;
81 // Call the API to create the app.
82 tasks.push(function(cb) {
83 invokeCreateAppApi(answers, function(err, app) {
84 if (err) return cb(err);
85 createdApp = app;
86 cb(null);
87 });
88 });
89
90 // Update the package.json
91 tasks.push(function(cb) {
92 manifest.update(appDir, createdApp, cb);
93 });
94
95 async.series(tasks, function(err) {
96 if (err) return done(err);
97
98 var message = "App created successfully at:\n" + createdApp.url +
99 "\n\n";
100
101 if (answers.startingMode === 'existing')
102 message += "To start developing run:\n$ 4front dev";
103 else
104 message += "To start developing run:\n$ cd " + createdApp.name +
105 "\n\nThen:\n$ 4front dev";
106
107 message +=
108 "\n\nWhen you are ready to deploy, simply run:\n$ 4front deploy";
109 log.messageBox(message);
110
111 done(null, createdApp);
112 });
113 });
114
115 function collectInput(callback) {
116 async.parallel({
117 templates: loadStarterTemplates,
118 organizations: loadOrganizations
119 }, function(err, results) {
120 if (err) return callback(err);
121
122 if (results.organizations.length == 0)
123 return callback(
124 "You need to belong to an organization to create a new app. Visit " + urljoin(program.profile.endpoint, '/portal/orgs/create') + " to get started."
125 );
126
127 promptQuestions(results, callback);
128 });
129 }
130
131 function loadStarterTemplates(callback) {
132 log.debug("fetching app templates");
133 api(program, {
134 method: 'GET',
135 path: '/platform/starter-templates'
136 }, function(err, templates) {
137 if (err) return callback(err);
138 callback(null, templates);
139 });
140 }
141
142 function loadOrganizations(callback) {
143 // Get the user's organizations
144 log.debug("fetching organizations");
145 api(program, {
146 method: 'GET',
147 path: '/profile/orgs'
148 }, function(err, orgs) {
149 if (err) return callback(err);
150 callback(null, orgs);
151 });
152 }
153
154 function promptQuestions(lookups, callback) {
155 var questions = [];
156
157 // Question to choose which organization the app belongs
158 if (_.isArray(lookups.organizations) && lookups.organizations.length > 0) {
159 questions.push(helper.pickOrgQuestion(lookups.organizations,
160 "Which organization does this app belong?"));
161 }
162
163 questions.push({
164 type: 'list',
165 name: 'startingMode',
166 choices: [{
167 name: 'Starting from scratch',
168 value: 'scratch'
169 }, {
170 name: 'Existing code',
171 value: 'existing'
172 }],
173 default: null,
174 when: function() {
175 // If a templateUrl was passed in from the command line we are
176 // by definition starting from scratch.
177 return !program.templateUrl;
178 },
179 message: "Are you starting this app from existing code or from scratch?"
180 });
181
182 // For existing code apps, have dev confirm that the current directory
183 // is where their app code is located.
184 questions.push({
185 type: 'confirm',
186 name: 'confirmExistingDir',
187 message: 'Is this directory ' + program.baseDir +
188 ' the location of your existing code?',
189 when: function(answers) {
190 return answers.startingMode === 'existing';
191 }
192 });
193
194 // Prompt user for which app template to start from
195 questions.push({
196 type: 'list',
197 message: 'Select app template to start from',
198 name: 'templateUrl',
199 when: function(answers) {
200 return answers.startingMode === 'scratch';
201 },
202 choices: buildTemplateChoices(lookups.templates)
203 });
204
205 inquirer.prompt(questions, function(answers) {
206 if (answers.confirmExistingDir === false)
207 return callback(answers);
208
209 collectAppName(function(err, appName) {
210 if (err) return callback(err);
211
212 answers.appName = appName;
213 callback(null, answers);
214 });
215 });
216 }
217
218 function collectAppName(callback) {
219 log.messageBox(
220 "Please choose a name for your app which will be used as\nthe URL, i.e. http://<app_name>." + program.virtualHost + ".\nNames may only contain lowercase letters, numbers,\nand dashes."
221 );
222
223 var question = {
224 type: 'input',
225 message: 'App name',
226 name: 'appName',
227 validate: function(input) {
228 if (!/^[a-z0-9\-]+$/.test(input))
229 return "Invalid app name";
230 else
231 return true;
232 }
233 };
234
235 var appName = null;
236
237 // Keep prompting for an appname until one is chosen that doesn't already exist.
238 async.until(function() {
239 return appName != null;
240 }, function(cb) {
241 inquirer.prompt([question], function(answers) {
242 appNameExists(answers.appName, function(err, exists) {
243 if (exists)
244 log.writeln(chalk.red(">>") + " App name \"" + answers.appName +
245 "\" is already taken. Please choose another name.");
246 else
247 appName = answers.appName;
248
249 cb();
250 });
251 });
252 }, function(err) {
253 if (err) return callback(err);
254 callback(null, appName);
255 });
256 }
257
258 function buildTemplateChoices(templates) {
259 var choices = [];
260 choices.push({
261 name: 'Blank',
262 value: 'blank'
263 });
264
265 templates.forEach(function(template, i) {
266 if (template.published !== false)
267 choices.push({
268 name: template.name,
269 value: template.url
270 });
271 });
272
273 return choices;
274 }
275
276 function npmInstall(appDir, callback) {
277 fs.exists(path.join(appDir, 'package.json'), function(exists) {
278 if (!exists)
279 return callback();
280
281 // If th node_modules directory already exists, assume npm install already run
282 if (fs.exists(path.join(appDir, 'node_modules')))
283 return callback();
284
285 log.info("Installing npm dependencies in %s", appDir);
286 spawn('npm', ['install'], {
287 cwd: appDir,
288 inheritStdio: true
289 }, callback);
290 });
291 }
292
293 function bowerInstall(appDir, callback) {
294 fs.exists(path.join(appDir, 'bower.json'), function(exists) {
295 if (!exists) {
296 log.debug("No bower.json file exists in app directory");
297 return callback();
298 }
299
300 log.info("Installing bower dependencies");
301 spawn('bower', ['install'], {
302 cwd: appDir,
303 inheritStdio: true
304 }, callback);
305 });
306 }
307
308 function unpackTemplate(templateUrl, appDir, callback) {
309 // Download, unzip, and extract the template.
310 log.info("Unpacking template %s to %s", templateUrl, appDir);
311
312 template.extract(templateUrl, appDir, callback);
313 }
314
315 function createBlankStart(answers, appDir, callback) {
316 // Create the bare minimum app code required which consists of a simple index.html page.
317 // The package.json will be created futher on in this flow.
318 var blankHtml = "<!DOCTYPE html>\n" +
319 "<html>\n" +
320 "\t<head>\n" +
321 "\t\t<title>Blank 4front App</title>\n" +
322 "\t</head>\n" +
323 "\t<body>\n" +
324 "\t\t<h1>Blank 4front App</h1>\n" +
325 "\t</body>" +
326 "</html>";
327
328 fs.writeFile(path.join(appDir, 'index.html'), blankHtml, callback);
329 }
330
331 function invokeCreateAppApi(answers, callback) {
332 var appData = {
333 name: answers.appName
334 };
335
336 if (answers.orgId)
337 appData.orgId = answers.orgId;
338
339 var options = {
340 method: 'POST',
341 path: '/apps',
342 json: appData
343 };
344
345 log.info("Invoking 4front API to create app");
346 var request = api(program, options, function(err, app) {
347 if (err) return callback(err);
348
349 log.debug("api post to /api/apps succeeded");
350 callback(null, app);
351 });
352 }
353
354 // Check if the specified app name is already in use by an app.
355 function appNameExists(appName, callback) {
356 var options = {
357 method: 'HEAD',
358 path: '/apps/' + appName
359 };
360
361 log.debug("checking if app name exists");
362 api(program, options, function(err, body, statusCode) {
363 if (err) return callback(err);
364
365 return callback(null, statusCode === 200);
366 });
367 }
368};