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