| /** |
| 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 fs = require('fs'); |
| var os = require('os'); |
| var path = require('path'); |
| |
| var Promise = require('q'); |
| var isUrl = require('is-url'); |
| var shell = require('shelljs'); |
| var requireFresh = require('import-fresh'); |
| var validateIdentifier = require('valid-identifier'); |
| |
| var fetch = require('cordova-fetch'); |
| var events = require('cordova-common').events; |
| var CordovaError = require('cordova-common').CordovaError; |
| var ConfigParser = require('cordova-common').ConfigParser; |
| var CordovaLogger = require('cordova-common').CordovaLogger.get(); |
| |
| const DEFAULT_VERSION = '1.0.0'; |
| |
| // Global configuration paths |
| var global_config_path = process.env.CORDOVA_HOME || path.join(os.homedir(), '.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) { |
| return Promise.resolve().then(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) { |
| cfg.lib.www.url = require.resolve('cordova-app-hello-world'); |
| cfg.lib.www.template = true; |
| } |
| |
| // 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.'); |
| |
| // If symlink, don't fetch |
| if (cfg.lib.www.link) { |
| return cfg.lib.www.url; |
| } |
| |
| var isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url); |
| var 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) |
| 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, {}) |
| .catch(function (err) { |
| events.emit('error', '\x1B[1m \x1B[31m Error from Cordova Fetch: ' + err.message); |
| events.emit('error', 'The template you are trying to use is invalid.' + |
| ' Make sure you follow the template guide found here https://cordova.apache.org/docs/en/latest/guide/cli/template.html.' + |
| ' Templates now require a package.json.'); |
| throw err; |
| }); |
| // If assets are not online, resolve as a relative path on local computer |
| } else { |
| return path.resolve(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 { |
| var templatePkg = requireFresh(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 dirAlreadyExisted = fs.existsSync(dir); |
| if (!dirAlreadyExisted) { |
| fs.mkdirSync(dir); |
| } |
| |
| try { |
| // Copy files from template to project |
| if (cfg.lib.www.template) { |
| events.emit('verbose', 'Copying assets.'); |
| 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) { |
| events.emit('verbose', 'Symlinking assets.'); |
| linkFromTemplate(import_from_path, dir); |
| } |
| |
| // If following were not copied/linked from template, copy from stock app hello world |
| // TODO: get stock package.json if template does not contain package.json; |
| copyIfNotExists(stockAssetPath('www'), path.join(dir, 'www')); |
| copyIfNotExists(stockAssetPath('hooks'), path.join(dir, 'hooks')); |
| var configXmlExists = projectConfig(dir); // moves config to root if in www |
| if (!configXmlExists) { |
| shell.cp(stockAssetPath('config.xml'), 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)) { |
| var pkgjson = requireFresh(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 { |
| // Use default name. |
| pkgjson.name = 'helloworld'; |
| } |
| |
| pkgjson.version = DEFAULT_VERSION; |
| fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8'); |
| } |
| |
| // Create basic project structure. |
| shell.mkdir('-p', path.join(dir, 'platforms')); |
| shell.mkdir('-p', 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, name and default version to config.xml |
| var conf = new ConfigParser(configPath); |
| if (cfg.id) conf.setPackageName(cfg.id); |
| if (cfg.name) conf.setName(cfg.name); |
| conf.setVersion(DEFAULT_VERSION); |
| 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 = 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); |
| } |
| } |
| } |
| |
| /** |
| * 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'); |
| var data; |
| if (!fs.existsSync(configPath)) { |
| data = '{}'; |
| } else { |
| data = fs.readFileSync(configPath, 'utf-8'); |
| } |
| return JSON.parse(data); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| function stockAssetPath (p) { |
| return path.join(require('cordova-app-hello-world').dirname, p); |
| } |