Remove dead code to handle missing cordova-app-hello-world
[cordova-create.git] / index.js
1 /**
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18 */
19
20 var path = require('path');
21 var fs = require('fs');
22 var shell = require('shelljs');
23 var events = require('cordova-common').events;
24 var Q = require('q');
25 var CordovaError = require('cordova-common').CordovaError;
26 var ConfigParser = require('cordova-common').ConfigParser;
27 var fetch = require('cordova-fetch');
28 var url = require('url');
29 var validateIdentifier = require('valid-identifier');
30 var CordovaLogger = require('cordova-common').CordovaLogger.get();
31
32 // Global configuration paths
33 var global_config_path = process.env.CORDOVA_HOME;
34 if (!global_config_path) {
35 var HOME = process.env[(process.platform.slice(0, 3) === 'win') ? 'USERPROFILE' : 'HOME'];
36 global_config_path = path.join(HOME, '.cordova');
37 }
38 /**
39 * Sets up to forward events to another instance, or log console.
40 * This will make the create internal events visible outside
41 * @param {EventEmitter} externalEventEmitter An EventEmitter instance that will be used for
42 * logging purposes. If no EventEmitter provided, all events will be logged to console
43 * @return {EventEmitter}
44 */
45 function setupEvents (externalEventEmitter) {
46 if (externalEventEmitter) {
47 // This will make the platform internal events visible outside
48 events.forwardEventsTo(externalEventEmitter);
49 // There is no logger if external emitter is not present,
50 // so attach a console logger
51 } else {
52 CordovaLogger.subscribe(events);
53 }
54 return events;
55 }
56
57 /**
58 * Usage:
59 * @dir - directory where the project will be created. Required.
60 * @optionalId - app id. Required (but be "undefined")
61 * @optionalName - app name. Required (but can be "undefined").
62 * @cfg - extra config to be saved in .cordova/config.json Required (but can be "{}").
63 * @extEvents - An EventEmitter instance that will be used for logging purposes. Required (but can be "undefined").
64 **/
65 // Returns a promise.
66 module.exports = function (dir, optionalId, optionalName, cfg, extEvents) {
67 return Q.fcall(function () {
68 events = setupEvents(extEvents);
69 events.emit('verbose', 'Using detached cordova-create');
70
71 if (!dir) {
72 throw new CordovaError('Directory not specified. See `cordova help`.');
73 }
74
75 // read projects .cordova/config.json file for project settings
76 var configFile = dotCordovaConfig(dir);
77
78 // if data exists in the configFile, lets combine it with cfg
79 // cfg values take priority over config file
80 if (configFile) {
81 var finalConfig = {};
82 for (var key1 in configFile) {
83 finalConfig[key1] = configFile[key1];
84 }
85
86 for (var key2 in cfg) {
87 finalConfig[key2] = cfg[key2];
88 }
89
90 cfg = finalConfig;
91 }
92
93 if (!cfg) {
94 throw new CordovaError('Must provide a project configuration.');
95 } else if (typeof cfg === 'string') {
96 cfg = JSON.parse(cfg);
97 }
98
99 if (optionalId) cfg.id = optionalId;
100 if (optionalName) cfg.name = optionalName;
101
102 // Make absolute.
103 dir = path.resolve(dir);
104
105 // dir must be either empty except for .cordova config file or not exist at all..
106 var sanedircontents = function (d) {
107 var contents = fs.readdirSync(d);
108 if (contents.length === 0) {
109 return true;
110 } else if (contents.length === 1) {
111 if (contents[0] === '.cordova') {
112 return true;
113 }
114 }
115 return false;
116 };
117
118 if (fs.existsSync(dir) && !sanedircontents(dir)) {
119 throw new CordovaError('Path already exists and is not empty: ' + dir);
120 }
121
122 if (cfg.id && !validateIdentifier(cfg.id)) {
123 throw new CordovaError('App id contains a reserved word, or is not a valid identifier.');
124 }
125
126 // This was changed from "uri" to "url", but checking uri for backwards compatibility.
127 cfg.lib = cfg.lib || {};
128 cfg.lib.www = cfg.lib.www || {};
129 cfg.lib.www.url = cfg.lib.www.url || cfg.lib.www.uri;
130
131 if (!cfg.lib.www.url) {
132 cfg.lib.www.url = require.resolve('cordova-app-hello-world');
133 cfg.lib.www.template = true;
134 }
135
136 // TODO (kamrik): extend lazy_load for retrieval without caching to allow net urls for --src.
137 cfg.lib.www.version = cfg.lib.www.version || 'not_versioned';
138 cfg.lib.www.id = cfg.lib.www.id || 'dummy_id';
139
140 // Make sure that the source www/ is not a direct ancestor of the
141 // target www/, or else we will recursively copy forever. To do this,
142 // we make sure that the shortest relative path from source-to-target
143 // must start by going up at least one directory or with a drive
144 // letter for Windows.
145 var rel_path = path.relative(cfg.lib.www.url, dir);
146 var goes_up = rel_path.split(path.sep)[0] === '..';
147
148 if (!(goes_up || rel_path[1] === ':')) {
149 throw new CordovaError(
150 'Project dir "' + dir +
151 '" must not be created at/inside the template used to create the project "' +
152 cfg.lib.www.url + '".'
153 );
154 }
155 })
156 .then(function () {
157 // Finally, Ready to start!
158 events.emit('log', 'Creating a new cordova project.');
159
160 // Strip link and url from cfg to avoid them being persisted to disk via .cordova/config.json.
161 // TODO: apparently underscore has no deep clone. Replace with lodash or something. For now, abuse JSON.
162 var cfgToPersistToDisk = JSON.parse(JSON.stringify(cfg));
163
164 delete cfgToPersistToDisk.lib.www;
165 if (Object.keys(cfgToPersistToDisk.lib).length === 0) {
166 delete cfgToPersistToDisk.lib;
167 }
168
169 // Update cached version of config.json
170 writeToConfigJson(dir, cfgToPersistToDisk, false);
171 })
172 .then(function () {
173 var isGit;
174 var isNPM;
175 var options;
176
177 // If symlink, don't fetch
178 if (cfg.lib.www.link) {
179 events.emit('verbose', 'Symlinking assets.');
180 return Q(cfg.lib.www.url);
181 }
182
183 events.emit('verbose', 'Copying assets."');
184 isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
185 isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url))) && !isGit;
186 // Always use cordova fetch to obtain the npm or git template
187 if (isGit || isNPM) {
188 // Saved to .Cordova folder (ToDo: Delete installed template after using)
189 // ToDo: @carynbear properly label errors from fetch as such
190 var tempDest = global_config_path;
191 var target = cfg.lib.www.url;
192 // add latest to npm module if no version is specified
193 // this prevents create using an older cached version of the template
194 if (isNPM && target.indexOf('@') === -1) {
195 target = cfg.lib.www.url + '@latest';
196 }
197 events.emit('verbose', 'Using cordova-fetch for ' + target);
198 return fetch(target, tempDest, {})
199 .fail(function (err) {
200 events.emit('error', '\x1B[1m \x1B[31m Error from Cordova Fetch: ' + err.message);
201 events.emit('error', 'The template you are trying to use is invalid.' +
202 ' Make sure you follow the template guide found here https://cordova.apache.org/docs/en/latest/guide/cli/template.html.' +
203 ' Templates now require a package.json.');
204 if (options.verbose) {
205 console.trace();
206 }
207 throw err;
208 });
209 // If assets are not online, resolve as a relative path on local computer
210 } else {
211 cfg.lib.www.url = path.resolve(cfg.lib.www.url);
212 return Q(cfg.lib.www.url);
213 }
214 })
215 .then(function (input_directory) {
216 var import_from_path = input_directory;
217
218 // handle when input wants to specify sub-directory (specified in index.js as "dirname" export);
219 var isSubDir = false;
220 try {
221 // Delete cached require incase one exists
222 delete require.cache[require.resolve(input_directory)];
223 var templatePkg = require(input_directory);
224 if (templatePkg && templatePkg.dirname) {
225 import_from_path = templatePkg.dirname;
226 isSubDir = true;
227 }
228 } catch (e) {
229 events.emit('verbose', 'index.js does not specify valid sub-directory: ' + input_directory);
230 isSubDir = false;
231 }
232
233 if (!fs.existsSync(import_from_path)) {
234 throw new CordovaError('Could not find directory: ' +
235 import_from_path);
236 }
237
238 var paths = {};
239
240 // get stock config.xml, used if template does not contain config.xml
241 paths.configXml = path.join(require('cordova-app-hello-world').dirname, 'config.xml');
242
243 // get stock www; used if template does not contain www
244 paths.www = path.join(require('cordova-app-hello-world').dirname, 'www');
245
246 // get stock hooks; used if template does not contain hooks
247 paths.hooks = path.join(require('cordova-app-hello-world').dirname, 'hooks');
248
249 // ToDo: get stock package.json if template does not contain package.json;
250 var dirAlreadyExisted = fs.existsSync(dir);
251 if (!dirAlreadyExisted) {
252 fs.mkdirSync(dir);
253 }
254
255 try {
256
257 // Copy files from template to project
258 if (cfg.lib.www.template) { copyTemplateFiles(import_from_path, dir, isSubDir); }
259
260 // If --link, link merges, hooks, www, and config.xml (and/or copy to root)
261 if (cfg.lib.www.link) { linkFromTemplate(import_from_path, dir); }
262
263 // If following were not copied/linked from template, copy from stock app hello world
264 copyIfNotExists(paths.www, path.join(dir, 'www'));
265 copyIfNotExists(paths.hooks, path.join(dir, 'hooks'));
266 var configXmlExists = projectConfig(dir); // moves config to root if in www
267 if (paths.configXml && !configXmlExists) {
268 shell.cp(paths.configXml, path.join(dir, 'config.xml'));
269 }
270 } catch (e) {
271 if (!dirAlreadyExisted) {
272 shell.rm('-rf', dir);
273 }
274 if (process.platform.slice(0, 3) === 'win' && e.code === 'EPERM') {
275 throw new CordovaError('Symlinks on Windows require Administrator privileges');
276 }
277 throw e;
278 }
279
280 var pkgjsonPath = path.join(dir, 'package.json');
281 // Update package.json name and version fields
282 if (fs.existsSync(pkgjsonPath)) {
283 delete require.cache[require.resolve(pkgjsonPath)];
284 var pkgjson = require(pkgjsonPath);
285
286 // Pkjson.displayName should equal config's name.
287 if (cfg.name) {
288 pkgjson.displayName = cfg.name;
289 }
290 // Pkjson.name should equal config's id.
291 if (cfg.id) {
292 pkgjson.name = cfg.id.toLowerCase();
293 } else if (!cfg.id) {
294 // Use default name.
295 pkgjson.name = 'helloworld';
296 }
297
298 pkgjson.version = '1.0.0';
299 fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8');
300 }
301
302 // Create basic project structure.
303 if (!fs.existsSync(path.join(dir, 'platforms'))) { shell.mkdir(path.join(dir, 'platforms')); }
304
305 if (!fs.existsSync(path.join(dir, 'plugins'))) { shell.mkdir(path.join(dir, 'plugins')); }
306
307 var configPath = path.join(dir, 'config.xml');
308 // only update config.xml if not a symlink
309 if (!fs.lstatSync(configPath).isSymbolicLink()) {
310 // Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
311 var conf = new ConfigParser(configPath);
312 if (cfg.id) conf.setPackageName(cfg.id);
313 if (cfg.name) conf.setName(cfg.name);
314 conf.setVersion('1.0.0');
315 conf.write();
316 }
317 });
318 };
319
320 /**
321 * Recursively copies folder to destination if folder is not found in destination (including symlinks).
322 * @param {string} src for copying
323 * @param {string} dst for copying
324 * @return No return value
325 */
326 function copyIfNotExists (src, dst) {
327 if (!fs.existsSync(dst) && src) {
328 shell.mkdir(dst);
329 shell.cp('-R', path.join(src, '*'), dst);
330 }
331 }
332
333 /**
334 * Copies template files, and directories into a Cordova project directory.
335 * If the template is a www folder, the www folder is simply copied
336 * Otherwise if the template exists in a subdirectory everything is copied
337 * Otherwise package.json, RELEASENOTES.md, .git, NOTICE, LICENSE, COPYRIGHT, and .npmignore are not copied over.
338 * A template directory, and project directory must be passed.
339 * templateDir - Template directory
340 * projectDir - Project directory
341 * isSubDir - boolean is true if template has subdirectory structure (see code around line 229)
342 */
343 function copyTemplateFiles (templateDir, projectDir, isSubDir) {
344 var copyPath;
345 // if template is a www dir
346 if (path.basename(templateDir) === 'www') {
347 copyPath = path.resolve(templateDir);
348 shell.cp('-R', copyPath, projectDir);
349 } else {
350 var templateFiles; // Current file
351 templateFiles = fs.readdirSync(templateDir);
352 // Remove directories, and files that are unwanted
353 if (!isSubDir) {
354 var excludes = ['package.json', 'RELEASENOTES.md', '.git', 'NOTICE', 'LICENSE', 'COPYRIGHT', '.npmignore'];
355 templateFiles = templateFiles.filter(function (value) {
356 return excludes.indexOf(value) < 0;
357 });
358 }
359 // Copy each template file after filter
360 for (var i = 0; i < templateFiles.length; i++) {
361 copyPath = path.resolve(templateDir, templateFiles[i]);
362 shell.cp('-R', copyPath, projectDir);
363 }
364 }
365 }
366
367 /**
368 * @param {String} value
369 * @return {Boolean} is the input value a url?
370 */
371 function isUrl (value) {
372 var u = value && url.parse(value);
373 return !!(u && u.protocol && u.protocol.length > 2); // Account for windows c:/ paths
374 }
375
376 /**
377 * Find config file in project directory or www directory
378 * If file is in www directory, move it outside
379 * @param {String} project directory to be searched
380 * @return {String or False} location of config file; if none exists, returns false
381 */
382 function projectConfig (projectDir) {
383 var rootPath = path.join(projectDir, 'config.xml');
384 var wwwPath = path.join(projectDir, 'www', 'config.xml');
385 if (fs.existsSync(rootPath)) {
386 return rootPath;
387 } else if (fs.existsSync(wwwPath)) {
388 fs.renameSync(wwwPath, rootPath);
389 return wwwPath;
390 }
391 return false;
392 }
393
394 /**
395 * Retrieve and read the .cordova/config file of a cordova project
396 *
397 * @param {String} project directory
398 * @return {JSON data} config file's contents
399 */
400 function dotCordovaConfig (project_root) {
401 var configPath = path.join(project_root, '.cordova', 'config.json');
402 var data;
403 if (!fs.existsSync(configPath)) {
404 data = '{}';
405 } else {
406 data = fs.readFileSync(configPath, 'utf-8');
407 }
408 return JSON.parse(data);
409 }
410
411 /**
412 * Write opts to .cordova/config.json
413 *
414 * @param {String} project directory
415 * @param {Object} opts containing the additions to config.json
416 * @param {Boolean} autopersist option
417 * @return {JSON Data}
418 */
419 function writeToConfigJson (project_root, opts, autoPersist) {
420 var json = dotCordovaConfig(project_root);
421 for (var p in opts) {
422 json[p] = opts[p];
423 }
424 if (autoPersist) {
425 var configPath = path.join(project_root, '.cordova', 'config.json');
426 var contents = JSON.stringify(json, null, 4);
427 // Don't write the file for an empty config.
428 if (contents !== '{}' || fs.existsSync(configPath)) {
429 shell.mkdir('-p', path.join(project_root, '.cordova'));
430 fs.writeFileSync(configPath, contents, 'utf-8');
431 }
432 return json;
433 } else {
434 return json;
435 }
436 }
437
438 /**
439 * Removes existing files and symlinks them if they exist.
440 * Symlinks folders: www, merges, hooks
441 * Symlinks file: config.xml (but only if it exists outside of the www folder)
442 * If config.xml exists inside of template/www, COPY (not link) it to project/
443 * */
444 function linkFromTemplate (templateDir, projectDir) {
445 var linkSrc, linkDst, linkFolders, copySrc, copyDst;
446 function rmlinkSync (src, dst, type) {
447 if (src && dst) {
448 if (fs.existsSync(dst)) {
449 shell.rm('-rf', dst);
450 }
451 if (fs.existsSync(src)) {
452 fs.symlinkSync(src, dst, type);
453 }
454 }
455 }
456 // if template is a www dir
457 if (path.basename(templateDir) === 'www') {
458 linkSrc = path.resolve(templateDir);
459 linkDst = path.join(projectDir, 'www');
460 rmlinkSync(linkSrc, linkDst, 'dir');
461 copySrc = path.join(templateDir, 'config.xml');
462 } else {
463 linkFolders = ['www', 'merges', 'hooks'];
464 // Link each folder
465 for (var i = 0; i < linkFolders.length; i++) {
466 linkSrc = path.join(templateDir, linkFolders[i]);
467 linkDst = path.join(projectDir, linkFolders[i]);
468 rmlinkSync(linkSrc, linkDst, 'dir');
469 }
470 linkSrc = path.join(templateDir, 'config.xml');
471 linkDst = path.join(projectDir, 'config.xml');
472 rmlinkSync(linkSrc, linkDst, 'file');
473 copySrc = path.join(templateDir, 'www', 'config.xml');
474 }
475 // if template/www/config.xml then copy to project/config.xml
476 copyDst = path.join(projectDir, 'config.xml');
477 if (!fs.existsSync(copyDst) && fs.existsSync(copySrc)) {
478 shell.cp(copySrc, projectDir);
479 }
480 }