| /** |
| 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. |
| */ |
| |
| const fs = require('fs-extra'); |
| |
| var path = require('path'); |
| |
| var tmp = require('tmp'); |
| var isUrl = require('is-url'); |
| var isObject = require('isobject'); |
| var pathIsInside = require('path-is-inside'); |
| 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'; |
| |
| /** |
| * 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; |
| } |
| |
| module.exports = cordovaCreateLegacyAdapter; |
| |
| /** |
| * Legacy interface. See README for documentation |
| */ |
| function cordovaCreateLegacyAdapter (dir, id, name, cfg, extEvents) { |
| // Unwrap and shallow-clone that nasty nested config object |
| const opts = Object.assign({}, ((cfg || {}).lib || {}).www); |
| |
| if (id) opts.id = id; |
| if (name) opts.name = name; |
| if (extEvents) opts.extEvents = extEvents; |
| |
| return cordovaCreate(dir, opts); |
| } |
| |
| /** |
| * Creates a new cordova project in the given directory. |
| * |
| * @param {string} dest directory where the project will be created. |
| * @param {Object} [opts={}] options to be used for creating the project. |
| * @returns {Promise} Resolves when project creation has finished. |
| */ |
| function cordovaCreate (dest, opts = {}) { |
| // TODO this is to avoid having a huge diff. Remove later. |
| let dir = dest; |
| |
| return Promise.resolve().then(function () { |
| if (!dir) { |
| throw new CordovaError('Directory not specified. See `cordova help`.'); |
| } |
| |
| if (!isObject(opts)) { |
| throw new CordovaError('Given options must be an object'); |
| } |
| |
| // Shallow copy opts |
| opts = Object.assign({}, opts); |
| |
| events = setupEvents(opts.extEvents); |
| events.emit('verbose', 'Using detached cordova-create'); |
| |
| // Make absolute. |
| dir = path.resolve(dir); |
| |
| if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0) { |
| throw new CordovaError('Path already exists and is not empty: ' + dir); |
| } |
| |
| if (opts.id && !validateIdentifier(opts.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. |
| opts.url = opts.url || opts.uri; |
| |
| if (!opts.url) { |
| opts.url = require.resolve('cordova-app-hello-world'); |
| opts.template = true; |
| } |
| |
| // Ensure that the destination is outside the template location |
| if (pathIsInside(dir, opts.url)) { |
| throw new CordovaError( |
| `Cannot create project "${dir}" inside the template used to create it "${opts.url}".` |
| ); |
| } |
| }) |
| .then(function () { |
| // Finally, Ready to start! |
| events.emit('log', 'Creating a new cordova project.'); |
| |
| // If symlink, don't fetch |
| if (opts.link) { |
| return opts.url; |
| } |
| |
| // Use cordova-fetch to obtain npm or git templates |
| if (opts.template && isRemoteUri(opts.url)) { |
| var target = opts.url; |
| events.emit('verbose', 'Using cordova-fetch for ' + target); |
| return fetch(target, getSelfDestructingTempDir(), {}); |
| } else { |
| // If assets are not online, resolve as a relative path on local computer |
| return path.resolve(opts.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 (opts.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 (opts.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) { |
| fs.copySync(stockAssetPath('config.xml'), path.join(dir, 'config.xml')); |
| } |
| } catch (e) { |
| if (!dirAlreadyExisted) { |
| fs.removeSync(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 (opts.name) { |
| pkgjson.displayName = opts.name; |
| } |
| // Pkjson.name should equal config's id. |
| if (opts.id) { |
| pkgjson.name = opts.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. |
| fs.ensureDirSync(path.join(dir, 'platforms')); |
| fs.ensureDirSync(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 (opts.id) conf.setPackageName(opts.id); |
| if (opts.name) conf.setName(opts.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) { |
| fs.copySync(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); |
| fs.copySync(copyPath, path.resolve(projectDir, 'www')); |
| } 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 |
| templateFiles.forEach(f => { |
| copyPath = path.resolve(templateDir, f); |
| fs.copySync(copyPath, path.resolve(projectDir, f)); |
| }); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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) { |
| fs.removeSync(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)) { |
| fs.copySync(copySrc, copyDst); |
| } |
| } |
| |
| function stockAssetPath (p) { |
| return path.join(require('cordova-app-hello-world').dirname, p); |
| } |
| |
| // Creates temp dir that is deleted on process exit |
| function getSelfDestructingTempDir () { |
| return tmp.dirSync({ |
| prefix: 'cordova-create-', |
| unsafeCleanup: true |
| }).name; |
| } |
| |
| function isRemoteUri (uri) { |
| return isUrl(uri) || uri.includes('@') || !fs.existsSync(uri); |
| } |