Remove dead code to update .cordova/config.json
[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 var isGit;
161 var isNPM;
162 var options;
163
164 // If symlink, don't fetch
165 if (cfg.lib.www.link) {
166 events.emit('verbose', 'Symlinking assets.');
167 return Q(cfg.lib.www.url);
168 }
169
170 events.emit('verbose', 'Copying assets."');
171 isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
172 isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url))) && !isGit;
173 // Always use cordova fetch to obtain the npm or git template
174 if (isGit || isNPM) {
175 // Saved to .Cordova folder (ToDo: Delete installed template after using)
176 // ToDo: @carynbear properly label errors from fetch as such
177 var tempDest = global_config_path;
178 var target = cfg.lib.www.url;
179 // add latest to npm module if no version is specified
180 // this prevents create using an older cached version of the template
181 if (isNPM && target.indexOf('@') === -1) {
182 target = cfg.lib.www.url + '@latest';
183 }
184 events.emit('verbose', 'Using cordova-fetch for ' + target);
185 return fetch(target, tempDest, {})
186 .fail(function (err) {
187 events.emit('error', '\x1B[1m \x1B[31m Error from Cordova Fetch: ' + err.message);
188 events.emit('error', 'The template you are trying to use is invalid.' +
189 ' Make sure you follow the template guide found here https://cordova.apache.org/docs/en/latest/guide/cli/template.html.' +
190 ' Templates now require a package.json.');
191 if (options.verbose) {
192 console.trace();
193 }
194 throw err;
195 });
196 // If assets are not online, resolve as a relative path on local computer
197 } else {
198 cfg.lib.www.url = path.resolve(cfg.lib.www.url);
199 return Q(cfg.lib.www.url);
200 }
201 })
202 .then(function (input_directory) {
203 var import_from_path = input_directory;
204
205 // handle when input wants to specify sub-directory (specified in index.js as "dirname" export);
206 var isSubDir = false;
207 try {
208 // Delete cached require incase one exists
209 delete require.cache[require.resolve(input_directory)];
210 var templatePkg = require(input_directory);
211 if (templatePkg && templatePkg.dirname) {
212 import_from_path = templatePkg.dirname;
213 isSubDir = true;
214 }
215 } catch (e) {
216 events.emit('verbose', 'index.js does not specify valid sub-directory: ' + input_directory);
217 isSubDir = false;
218 }
219
220 if (!fs.existsSync(import_from_path)) {
221 throw new CordovaError('Could not find directory: ' +
222 import_from_path);
223 }
224
225 var paths = {};
226
227 // get stock config.xml, used if template does not contain config.xml
228 paths.configXml = path.join(require('cordova-app-hello-world').dirname, 'config.xml');
229
230 // get stock www; used if template does not contain www
231 paths.www = path.join(require('cordova-app-hello-world').dirname, 'www');
232
233 // get stock hooks; used if template does not contain hooks
234 paths.hooks = path.join(require('cordova-app-hello-world').dirname, 'hooks');
235
236 // ToDo: get stock package.json if template does not contain package.json;
237 var dirAlreadyExisted = fs.existsSync(dir);
238 if (!dirAlreadyExisted) {
239 fs.mkdirSync(dir);
240 }
241
242 try {
243
244 // Copy files from template to project
245 if (cfg.lib.www.template) { copyTemplateFiles(import_from_path, dir, isSubDir); }
246
247 // If --link, link merges, hooks, www, and config.xml (and/or copy to root)
248 if (cfg.lib.www.link) { linkFromTemplate(import_from_path, dir); }
249
250 // If following were not copied/linked from template, copy from stock app hello world
251 copyIfNotExists(paths.www, path.join(dir, 'www'));
252 copyIfNotExists(paths.hooks, path.join(dir, 'hooks'));
253 var configXmlExists = projectConfig(dir); // moves config to root if in www
254 if (paths.configXml && !configXmlExists) {
255 shell.cp(paths.configXml, path.join(dir, 'config.xml'));
256 }
257 } catch (e) {
258 if (!dirAlreadyExisted) {
259 shell.rm('-rf', dir);
260 }
261 if (process.platform.slice(0, 3) === 'win' && e.code === 'EPERM') {
262 throw new CordovaError('Symlinks on Windows require Administrator privileges');
263 }
264 throw e;
265 }
266
267 var pkgjsonPath = path.join(dir, 'package.json');
268 // Update package.json name and version fields
269 if (fs.existsSync(pkgjsonPath)) {
270 delete require.cache[require.resolve(pkgjsonPath)];
271 var pkgjson = require(pkgjsonPath);
272
273 // Pkjson.displayName should equal config's name.
274 if (cfg.name) {
275 pkgjson.displayName = cfg.name;
276 }
277 // Pkjson.name should equal config's id.
278 if (cfg.id) {
279 pkgjson.name = cfg.id.toLowerCase();
280 } else if (!cfg.id) {
281 // Use default name.
282 pkgjson.name = 'helloworld';
283 }
284
285 pkgjson.version = '1.0.0';
286 fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8');
287 }
288
289 // Create basic project structure.
290 if (!fs.existsSync(path.join(dir, 'platforms'))) { shell.mkdir(path.join(dir, 'platforms')); }
291
292 if (!fs.existsSync(path.join(dir, 'plugins'))) { shell.mkdir(path.join(dir, 'plugins')); }
293
294 var configPath = path.join(dir, 'config.xml');
295 // only update config.xml if not a symlink
296 if (!fs.lstatSync(configPath).isSymbolicLink()) {
297 // Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
298 var conf = new ConfigParser(configPath);
299 if (cfg.id) conf.setPackageName(cfg.id);
300 if (cfg.name) conf.setName(cfg.name);
301 conf.setVersion('1.0.0');
302 conf.write();
303 }
304 });
305 };
306
307 /**
308 * Recursively copies folder to destination if folder is not found in destination (including symlinks).
309 * @param {string} src for copying
310 * @param {string} dst for copying
311 * @return No return value
312 */
313 function copyIfNotExists (src, dst) {
314 if (!fs.existsSync(dst) && src) {
315 shell.mkdir(dst);
316 shell.cp('-R', path.join(src, '*'), dst);
317 }
318 }
319
320 /**
321 * Copies template files, and directories into a Cordova project directory.
322 * If the template is a www folder, the www folder is simply copied
323 * Otherwise if the template exists in a subdirectory everything is copied
324 * Otherwise package.json, RELEASENOTES.md, .git, NOTICE, LICENSE, COPYRIGHT, and .npmignore are not copied over.
325 * A template directory, and project directory must be passed.
326 * templateDir - Template directory
327 * projectDir - Project directory
328 * isSubDir - boolean is true if template has subdirectory structure (see code around line 229)
329 */
330 function copyTemplateFiles (templateDir, projectDir, isSubDir) {
331 var copyPath;
332 // if template is a www dir
333 if (path.basename(templateDir) === 'www') {
334 copyPath = path.resolve(templateDir);
335 shell.cp('-R', copyPath, projectDir);
336 } else {
337 var templateFiles; // Current file
338 templateFiles = fs.readdirSync(templateDir);
339 // Remove directories, and files that are unwanted
340 if (!isSubDir) {
341 var excludes = ['package.json', 'RELEASENOTES.md', '.git', 'NOTICE', 'LICENSE', 'COPYRIGHT', '.npmignore'];
342 templateFiles = templateFiles.filter(function (value) {
343 return excludes.indexOf(value) < 0;
344 });
345 }
346 // Copy each template file after filter
347 for (var i = 0; i < templateFiles.length; i++) {
348 copyPath = path.resolve(templateDir, templateFiles[i]);
349 shell.cp('-R', copyPath, projectDir);
350 }
351 }
352 }
353
354 /**
355 * @param {String} value
356 * @return {Boolean} is the input value a url?
357 */
358 function isUrl (value) {
359 var u = value && url.parse(value);
360 return !!(u && u.protocol && u.protocol.length > 2); // Account for windows c:/ paths
361 }
362
363 /**
364 * Find config file in project directory or www directory
365 * If file is in www directory, move it outside
366 * @param {String} project directory to be searched
367 * @return {String or False} location of config file; if none exists, returns false
368 */
369 function projectConfig (projectDir) {
370 var rootPath = path.join(projectDir, 'config.xml');
371 var wwwPath = path.join(projectDir, 'www', 'config.xml');
372 if (fs.existsSync(rootPath)) {
373 return rootPath;
374 } else if (fs.existsSync(wwwPath)) {
375 fs.renameSync(wwwPath, rootPath);
376 return wwwPath;
377 }
378 return false;
379 }
380
381 /**
382 * Retrieve and read the .cordova/config file of a cordova project
383 *
384 * @param {String} project directory
385 * @return {JSON data} config file's contents
386 */
387 function dotCordovaConfig (project_root) {
388 var configPath = path.join(project_root, '.cordova', 'config.json');
389 var data;
390 if (!fs.existsSync(configPath)) {
391 data = '{}';
392 } else {
393 data = fs.readFileSync(configPath, 'utf-8');
394 }
395 return JSON.parse(data);
396 }
397
398 /**
399 * Removes existing files and symlinks them if they exist.
400 * Symlinks folders: www, merges, hooks
401 * Symlinks file: config.xml (but only if it exists outside of the www folder)
402 * If config.xml exists inside of template/www, COPY (not link) it to project/
403 * */
404 function linkFromTemplate (templateDir, projectDir) {
405 var linkSrc, linkDst, linkFolders, copySrc, copyDst;
406 function rmlinkSync (src, dst, type) {
407 if (src && dst) {
408 if (fs.existsSync(dst)) {
409 shell.rm('-rf', dst);
410 }
411 if (fs.existsSync(src)) {
412 fs.symlinkSync(src, dst, type);
413 }
414 }
415 }
416 // if template is a www dir
417 if (path.basename(templateDir) === 'www') {
418 linkSrc = path.resolve(templateDir);
419 linkDst = path.join(projectDir, 'www');
420 rmlinkSync(linkSrc, linkDst, 'dir');
421 copySrc = path.join(templateDir, 'config.xml');
422 } else {
423 linkFolders = ['www', 'merges', 'hooks'];
424 // Link each folder
425 for (var i = 0; i < linkFolders.length; i++) {
426 linkSrc = path.join(templateDir, linkFolders[i]);
427 linkDst = path.join(projectDir, linkFolders[i]);
428 rmlinkSync(linkSrc, linkDst, 'dir');
429 }
430 linkSrc = path.join(templateDir, 'config.xml');
431 linkDst = path.join(projectDir, 'config.xml');
432 rmlinkSync(linkSrc, linkDst, 'file');
433 copySrc = path.join(templateDir, 'www', 'config.xml');
434 }
435 // if template/www/config.xml then copy to project/config.xml
436 copyDst = path.join(projectDir, 'config.xml');
437 if (!fs.existsSync(copyDst) && fs.existsSync(copySrc)) {
438 shell.cp(copySrc, projectDir);
439 }
440 }