| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| var path = require('path'), |
| fs = require('fs'), |
| shell = require('shelljs'), |
| events = require('cordova-common').events, |
| Q = require('q'), |
| CordovaError = require('cordova-common').CordovaError, |
| ConfigParser = require('cordova-common').ConfigParser, |
| fetch = require('cordova-fetch'), |
| url = require('url'), |
| validateIdentifier = require('valid-identifier'), |
| CordovaLogger = require('cordova-common').CordovaLogger.get(); |
| |
| // Global configuration paths |
| var global_config_path = process.env.CORDOVA_HOME; |
| if (!global_config_path) { |
| var HOME = process.env[(process.platform.slice(0, 3) == 'win') ? 'USERPROFILE' : 'HOME']; |
| global_config_path = path.join(HOME, '.cordova'); |
| } |
| /** |
| * Sets up to forward events to another instance, or log console. |
| * This will make the create internal events visible outside |
| * @param {EventEmitter} externalEventEmitter An EventEmitter instance that will be used for |
| * logging purposes. If no EventEmitter provided, all events will be logged to console |
| * @return {EventEmitter} |
| */ |
| function setupEvents(externalEventEmitter) { |
| if (externalEventEmitter) { |
| // This will make the platform internal events visible outside |
| events.forwardEventsTo(externalEventEmitter); |
| } |
| // There is no logger if external emitter is not present, |
| // so attach a console logger |
| else { |
| CordovaLogger.subscribe(events); |
| } |
| return events; |
| } |
| |
| /** |
| * Usage: |
| * @dir - directory where the project will be created. Required. |
| * @optionalId - app id. Required (but be "undefined") |
| * @optionalName - app name. Required (but can be "undefined"). |
| * @cfg - extra config to be saved in .cordova/config.json Required (but can be "{}"). |
| * @extEvents - An EventEmitter instance that will be used for logging purposes. Required (but can be "undefined"). |
| **/ |
| // Returns a promise. |
| module.exports = function(dir, optionalId, optionalName, cfg, extEvents) { |
| var argumentCount = arguments.length; |
| return Q.fcall(function() { |
| events = setupEvents(extEvents); |
| events.emit('verbose', 'Using detached cordova-create'); |
| |
| if (!dir) { |
| throw new CordovaError('Directory not specified. See `cordova help`.'); |
| } |
| |
| //read projects .cordova/config.json file for project settings |
| var configFile = dotCordovaConfig(dir); |
| |
| //if data exists in the configFile, lets combine it with cfg |
| //cfg values take priority over config file |
| if(configFile) { |
| var finalConfig = {}; |
| for(var key1 in configFile) { |
| finalConfig[key1] = configFile[key1]; |
| } |
| |
| for(var key2 in cfg) { |
| finalConfig[key2] = cfg[key2]; |
| } |
| |
| cfg = finalConfig; |
| } |
| |
| if (!cfg) { |
| throw new CordovaError('Must provide a project configuration.'); |
| } else if (typeof cfg == 'string') { |
| cfg = JSON.parse(cfg); |
| } |
| |
| if (optionalId) cfg.id = optionalId; |
| if (optionalName) cfg.name = optionalName; |
| |
| // Make absolute. |
| dir = path.resolve(dir); |
| |
| // dir must be either empty except for .cordova config file or not exist at all.. |
| var sanedircontents = function (d) { |
| var contents = fs.readdirSync(d); |
| if (contents.length === 0) { |
| return true; |
| } else if (contents.length == 1) { |
| if (contents[0] == '.cordova') { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| if (fs.existsSync(dir) && !sanedircontents(dir)) { |
| throw new CordovaError('Path already exists and is not empty: ' + dir); |
| } |
| |
| if (cfg.id && !validateIdentifier(cfg.id)) { |
| throw new CordovaError('App id contains a reserved word, or is not a valid identifier.'); |
| } |
| |
| |
| // This was changed from "uri" to "url", but checking uri for backwards compatibility. |
| cfg.lib = cfg.lib || {}; |
| cfg.lib.www = cfg.lib.www || {}; |
| cfg.lib.www.url = cfg.lib.www.url || cfg.lib.www.uri; |
| |
| if (!cfg.lib.www.url) { |
| try { |
| cfg.lib.www.url = require('cordova-app-hello-world').dirname; |
| } catch (e) { |
| // Falling back on npm@2 path hierarchy |
| // TODO: Remove fallback after cordova-app-hello-world release |
| cfg.lib.www.url = path.join(__dirname, '..', '..', 'node_modules', 'cordova-app-hello-world'); |
| } |
| } |
| |
| // TODO (kamrik): extend lazy_load for retrieval without caching to allow net urls for --src. |
| cfg.lib.www.version = cfg.lib.www.version || 'not_versioned'; |
| cfg.lib.www.id = cfg.lib.www.id || 'dummy_id'; |
| |
| // Make sure that the source www/ is not a direct ancestor of the |
| // target www/, or else we will recursively copy forever. To do this, |
| // we make sure that the shortest relative path from source-to-target |
| // must start by going up at least one directory or with a drive |
| // letter for Windows. |
| var rel_path = path.relative(cfg.lib.www.url, dir); |
| var goes_up = rel_path.split(path.sep)[0] == '..'; |
| |
| if (!(goes_up || rel_path[1] == ':')) { |
| throw new CordovaError( |
| 'Project dir "' + dir + |
| '" must not be created at/inside the template used to create the project "' + |
| cfg.lib.www.url + '".' |
| ); |
| } |
| }) |
| .then(function() { |
| // Finally, Ready to start! |
| events.emit('log', 'Creating a new cordova project.'); |
| |
| // Strip link and url from cfg to avoid them being persisted to disk via .cordova/config.json. |
| // TODO: apparently underscore has no deep clone. Replace with lodash or something. For now, abuse JSON. |
| var cfgToPersistToDisk = JSON.parse(JSON.stringify(cfg)); |
| |
| delete cfgToPersistToDisk.lib.www; |
| if (Object.keys(cfgToPersistToDisk.lib).length === 0) { |
| delete cfgToPersistToDisk.lib; |
| } |
| |
| // Update cached version of config.json |
| writeToConfigJson(dir, cfgToPersistToDisk, false); |
| }) |
| .then(function() { |
| var gitURL; |
| var branch; |
| var parseArr; |
| var packageName; |
| var packageVersion; |
| var isGit; |
| var isNPM; |
| |
| //If symlink, don't fetch |
| if (!!cfg.lib.www.link) { |
| events.emit('verbose', 'Symlinking assets.'); |
| return Q(cfg.lib.www.url); |
| } |
| |
| events.emit('verbose', 'Copying assets."'); |
| isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url); |
| isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url))) && !isGit; |
| //Always use cordova fetch to obtain the npm or git template |
| if (isGit || isNPM) { |
| //Saved to .Cordova folder (ToDo: Delete installed template after using) |
| //ToDo: @carynbear properly label errors from fetch as such |
| var tempDest = global_config_path; |
| var target = cfg.lib.www.url; |
| //add latest to npm module if no version is specified |
| //this prevents create using an older cached version of the template |
| if(isNPM && target.indexOf('@') === -1) { |
| target = cfg.lib.www.url + '@latest'; |
| } |
| events.emit('verbose', 'Using cordova-fetch for '+ target); |
| return fetch(target, tempDest, {}) |
| .fail(function(err){ |
| events.emit('error', '\033[1m \033[31m Error from Cordova Fetch: ' + err.message); |
| if (options.verbose) { |
| console.trace(); |
| } |
| throw err; |
| }); |
| //If assets are not online, resolve as a relative path on local computer |
| } else { |
| cfg.lib.www.url = path.resolve(cfg.lib.www.url); |
| return Q(cfg.lib.www.url); |
| } |
| }).then(function(input_directory) { |
| var import_from_path = input_directory; |
| |
| //handle when input wants to specify sub-directory (specified in index.js as "dirname" export); |
| var isSubDir = false; |
| try { |
| // Delete cached require incase one exists |
| delete require.cache[require.resolve(input_directory)]; |
| var templatePkg = require(input_directory); |
| if (templatePkg && templatePkg.dirname){ |
| import_from_path = templatePkg.dirname; |
| isSubDir = true; |
| } |
| } catch (e) { |
| events.emit('verbose', 'index.js does not specify valid sub-directory: ' + input_directory); |
| isSubDir = false; |
| } |
| |
| if (!fs.existsSync(import_from_path)) { |
| throw new CordovaError('Could not find directory: ' + |
| import_from_path); |
| } |
| |
| var paths = {}; |
| |
| // get stock config.xml, used if template does not contain config.xml |
| paths.configXml = path.join(require('cordova-app-hello-world').dirname, 'config.xml'); |
| |
| // get stock www; used if template does not contain www |
| paths.www = path.join(require('cordova-app-hello-world').dirname, 'www'); |
| |
| // get stock hooks; used if template does not contain hooks |
| paths.hooks = path.join(require('cordova-app-hello-world').dirname, 'hooks'); |
| |
| // ToDo: get stock package.json if template does not contain package.json; |
| |
| var dirAlreadyExisted = fs.existsSync(dir); |
| if (!dirAlreadyExisted) { |
| fs.mkdirSync(dir); |
| } |
| |
| try { |
| |
| // Copy files from template to project |
| if (cfg.lib.www.template) |
| copyTemplateFiles(import_from_path, dir, isSubDir); |
| |
| // If --link, link merges, hooks, www, and config.xml (and/or copy to root) |
| if (!!cfg.lib.www.link) |
| linkFromTemplate(import_from_path, dir); |
| |
| // If following were not copied/linked from template, copy from stock app hello world |
| copyIfNotExists(paths.www, path.join(dir, 'www')); |
| copyIfNotExists(paths.hooks, path.join(dir, 'hooks')); |
| var configXmlExists = projectConfig(dir); //moves config to root if in www |
| if (paths.configXml && !configXmlExists) { |
| shell.cp(paths.configXml, path.join(dir, 'config.xml')); |
| } |
| } catch (e) { |
| if (!dirAlreadyExisted) { |
| shell.rm('-rf', dir); |
| } |
| if (process.platform.slice(0, 3) == 'win' && e.code == 'EPERM') { |
| throw new CordovaError('Symlinks on Windows require Administrator privileges'); |
| } |
| throw e; |
| } |
| |
| var pkgjsonPath = path.join(dir, 'package.json'); |
| // Update package.json name and version fields |
| if (fs.existsSync(pkgjsonPath)) { |
| delete require.cache[require.resolve(pkgjsonPath)]; |
| var pkgjson = require(pkgjsonPath); |
| |
| // Pkjson.displayName should equal config's name. |
| if (cfg.name) { |
| pkgjson.displayName = cfg.name; |
| } |
| // Pkjson.name should equal config's id. |
| if(cfg.id) { |
| pkgjson.name = cfg.id.toLowerCase(); |
| } else if(!cfg.id) { |
| // Use default name. |
| pkgjson.name = 'helloworld'; |
| } |
| |
| pkgjson.version = '1.0.0'; |
| fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8'); |
| } |
| |
| // Create basic project structure. |
| if (!fs.existsSync(path.join(dir, 'platforms'))) |
| shell.mkdir(path.join(dir, 'platforms')); |
| |
| if (!fs.existsSync(path.join(dir, 'plugins'))) |
| shell.mkdir(path.join(dir, 'plugins')); |
| |
| var configPath = path.join(dir, 'config.xml'); |
| // only update config.xml if not a symlink |
| if(!fs.lstatSync(configPath).isSymbolicLink()) { |
| // Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version) |
| var conf = new ConfigParser(configPath); |
| if (cfg.id) conf.setPackageName(cfg.id); |
| if (cfg.name) conf.setName(cfg.name); |
| conf.setVersion('1.0.0'); |
| conf.write(); |
| } |
| }); |
| }; |
| |
| /** |
| * Recursively copies folder to destination if folder is not found in destination (including symlinks). |
| * @param {string} src for copying |
| * @param {string} dst for copying |
| * @return No return value |
| */ |
| function copyIfNotExists(src, dst) { |
| if (!fs.existsSync(dst) && src) { |
| shell.mkdir(dst); |
| shell.cp('-R', path.join(src, '*'), dst); |
| } |
| } |
| |
| /** |
| * Copies template files, and directories into a Cordova project directory. |
| * If the template is a www folder, the www folder is simply copied |
| * Otherwise if the template exists in a subdirectory everything is copied |
| * Otherwise package.json, RELEASENOTES.md, .git, NOTICE, LICENSE, COPYRIGHT, and .npmignore are not copied over. |
| * A template directory, and project directory must be passed. |
| * templateDir - Template directory |
| * projectDir - Project directory |
| * isSubDir - boolean is true if template has subdirectory structure (see code around line 229) |
| */ |
| function copyTemplateFiles(templateDir, projectDir, isSubDir) { |
| var copyPath; |
| // if template is a www dir |
| if (path.basename(templateDir) === 'www') { |
| copyPath = path.resolve(templateDir); |
| shell.cp('-R', copyPath, projectDir); |
| } else { |
| var templateFiles; // Current file |
| templateFiles = fs.readdirSync(templateDir); |
| // Remove directories, and files that are unwanted |
| if (!isSubDir) { |
| var excludes = ['package.json', 'RELEASENOTES.md' , '.git', 'NOTICE', 'LICENSE', 'COPYRIGHT', '.npmignore']; |
| templateFiles = templateFiles.filter( function (value) { |
| return excludes.indexOf(value) < 0; |
| }); |
| } |
| // Copy each template file after filter |
| for (var i = 0; i < templateFiles.length; i++) { |
| copyPath = path.resolve(templateDir, templateFiles[i]); |
| shell.cp('-R', copyPath, projectDir); |
| } |
| } |
| } |
| |
| /** |
| * @param {String} value |
| * @return {Boolean} is the input value a url? |
| */ |
| function isUrl(value) { |
| var u = value && url.parse(value); |
| return !!(u && u.protocol && u.protocol.length > 2); // Account for windows c:/ paths |
| } |
| |
| /** |
| * Find config file in project directory or www directory |
| * If file is in www directory, move it outside |
| * @param {String} project directory to be searched |
| * @return {String or False} location of config file; if none exists, returns false |
| */ |
| function projectConfig(projectDir) { |
| var rootPath = path.join(projectDir, 'config.xml'); |
| var wwwPath = path.join(projectDir, 'www', 'config.xml'); |
| if (fs.existsSync(rootPath)) { |
| return rootPath; |
| } else if (fs.existsSync(wwwPath)) { |
| fs.renameSync(wwwPath, rootPath); |
| return wwwPath; |
| } |
| return false; |
| } |
| |
| /** |
| * Retrieve and read the .cordova/config file of a cordova project |
| * |
| * @param {String} project directory |
| * @return {JSON data} config file's contents |
| */ |
| function dotCordovaConfig(project_root) { |
| var configPath = path.join(project_root, '.cordova', 'config.json'); |
| if (!fs.existsSync(configPath)) { |
| data = '{}'; |
| } else { |
| data = fs.readFileSync(configPath, 'utf-8'); |
| } |
| return JSON.parse(data); |
| } |
| |
| /** |
| * Write opts to .cordova/config.json |
| * |
| * @param {String} project directory |
| * @param {Object} opts containing the additions to config.json |
| * @param {Boolean} autopersist option |
| * @return {JSON Data} |
| */ |
| function writeToConfigJson(project_root, opts, autoPersist) { |
| var json = dotCordovaConfig(project_root); |
| for (var p in opts) { |
| json[p] = opts[p]; |
| } |
| if (autoPersist) { |
| var configPath = path.join(project_root, '.cordova', 'config.json'); |
| var contents = JSON.stringify(json, null, 4); |
| // Don't write the file for an empty config. |
| if (contents != '{}' || fs.existsSync(configPath)) { |
| shell.mkdir('-p', path.join(project_root, '.cordova')); |
| fs.writeFileSync(configPath, contents, 'utf-8'); |
| } |
| return json; |
| } else { |
| return json; |
| } |
| } |
| |
| /** |
| * Removes existing files and symlinks them if they exist. |
| * Symlinks folders: www, merges, hooks |
| * Symlinks file: config.xml (but only if it exists outside of the www folder) |
| * If config.xml exists inside of template/www, COPY (not link) it to project/ |
| * */ |
| function linkFromTemplate(templateDir, projectDir) { |
| var linkSrc, linkDst, linkFolders, copySrc, copyDst; |
| function rmlinkSync(src, dst, type) { |
| if (src && dst) { |
| if (fs.existsSync(dst)) { |
| shell.rm('-rf', dst); |
| } |
| if (fs.existsSync(src)) { |
| fs.symlinkSync(src, dst, type); |
| } |
| } |
| } |
| // if template is a www dir |
| if (path.basename(templateDir) === 'www') { |
| linkSrc = path.resolve(templateDir); |
| linkDst = path.join(projectDir, 'www'); |
| rmlinkSync(linkSrc, linkDst, 'dir'); |
| copySrc = path.join(templateDir, 'config.xml'); |
| } else { |
| linkFolders = ['www', 'merges', 'hooks']; |
| // Link each folder |
| for (var i = 0; i < linkFolders.length; i++) { |
| linkSrc = path.join(templateDir, linkFolders[i]); |
| linkDst = path.join(projectDir, linkFolders[i]); |
| rmlinkSync(linkSrc, linkDst, 'dir'); |
| } |
| linkSrc = path.join(templateDir, 'config.xml'); |
| linkDst = path.join(projectDir, 'config.xml'); |
| rmlinkSync(linkSrc, linkDst, 'file'); |
| copySrc = path.join(templateDir, 'www', 'config.xml'); |
| } |
| // if template/www/config.xml then copy to project/config.xml |
| copyDst = path.join(projectDir, 'config.xml'); |
| if (!fs.existsSync(copyDst) && fs.existsSync(copySrc)) { |
| shell.cp(copySrc, projectDir); |
| } |
| } |