1 | var chalk = require('chalk');
|
2 | var async = require('async');
|
3 | var request = require('request');
|
4 | var _ = require('lodash');
|
5 | var fs = require('fs');
|
6 | var urljoin = require('url-join');
|
7 | var path = require('path');
|
8 | var inquirer = require('inquirer');
|
9 | var spawn = require('../lib/spawn');
|
10 | var api = require('../lib/api');
|
11 | var log = require('../lib/log');
|
12 | var debug = require('debug')('4front:cli:create-app');
|
13 | var manifest = require('../lib/manifest');
|
14 | var template = require('../lib/template');
|
15 | var helper = require('../lib/helper');
|
16 |
|
17 | require("simple-errors");
|
18 |
|
19 | module.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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
176 |
|
177 | return !program.templateUrl;
|
178 | },
|
179 | message: "Are you starting this app from existing code or from scratch?"
|
180 | });
|
181 |
|
182 |
|
183 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
317 |
|
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 |
|
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 | };
|