UNPKG

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