diff --git a/LICENSE b/LICENSE index c47288d4b2..61af2bb868 100644 --- a/LICENSE +++ b/LICENSE @@ -226,36 +226,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -================================================================================ -bin/node_modules/shelljs -================================================================================ -Copyright (c) 2012, Artur Adib -All rights reserved. - -You may use this project under the terms of the New BSD license as follows: - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Artur Adib nor the - names of the contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ================================================================================ bin/node_modules/nopt ================================================================================ diff --git a/bin/lib/create.js b/bin/lib/create.js index d737b32829..c9670348de 100755 --- a/bin/lib/create.js +++ b/bin/lib/create.js @@ -19,9 +19,9 @@ under the License. */ -var shell = require('shelljs'); var path = require('path'); -var fs = require('fs'); +var fs = require('fs-extra'); +var utils = require('../../bin/lib/utils'); var check_reqs = require('./../templates/cordova/lib/check_reqs'); var ROOT = path.join(__dirname, '..', '..'); @@ -33,20 +33,12 @@ var AndroidManifest = require('../templates/cordova/lib/AndroidManifest'); // (since we can then mock and control behaviour of all of these functions) exports.validatePackageName = validatePackageName; exports.validateProjectName = validateProjectName; -exports.setShellFatal = setShellFatal; exports.copyJsAndLibrary = copyJsAndLibrary; exports.copyScripts = copyScripts; exports.copyBuildRules = copyBuildRules; exports.writeProjectProperties = writeProjectProperties; exports.prepBuildFiles = prepBuildFiles; -function setShellFatal (value, func) { - var oldVal = shell.config.fatal; - shell.config.fatal = value; - func(); - shell.config.fatal = oldVal; -} - function getFrameworkDir (projectPath, shared) { return shared ? path.join(ROOT, 'framework') : path.join(projectPath, 'CordovaLib'); } @@ -55,53 +47,33 @@ function copyJsAndLibrary (projectPath, shared, projectName, isLegacy) { var nestedCordovaLibPath = getFrameworkDir(projectPath, false); var srcCordovaJsPath = path.join(ROOT, 'bin', 'templates', 'project', 'assets', 'www', 'cordova.js'); var app_path = path.join(projectPath, 'app', 'src', 'main'); + const platform_www = path.join(projectPath, 'platform_www'); if (isLegacy) { app_path = projectPath; } - shell.cp('-f', srcCordovaJsPath, path.join(app_path, 'assets', 'www', 'cordova.js')); + fs.copySync(srcCordovaJsPath, path.join(app_path, 'assets', 'www', 'cordova.js')); // Copy the cordova.js file to platforms//platform_www/ // The www dir is nuked on each prepare so we keep cordova.js in platform_www - shell.mkdir('-p', path.join(projectPath, 'platform_www')); - shell.cp('-f', srcCordovaJsPath, path.join(projectPath, 'platform_www')); + fs.ensureDirSync(platform_www); + fs.copySync(srcCordovaJsPath, path.join(platform_www, 'cordova.js')); // Copy cordova-js-src directory into platform_www directory. // We need these files to build cordova.js if using browserify method. - shell.cp('-rf', path.join(ROOT, 'cordova-js-src'), path.join(projectPath, 'platform_www')); - - // Don't fail if there are no old jars. - exports.setShellFatal(false, function () { - shell.ls(path.join(app_path, 'libs', 'cordova-*.jar')).forEach(function (oldJar) { - console.log('Deleting ' + oldJar); - shell.rm('-f', oldJar); - }); - var wasSymlink = true; - try { - // Delete the symlink if it was one. - fs.unlinkSync(nestedCordovaLibPath); - } catch (e) { - wasSymlink = false; - } - // Delete old library project if it existed. - if (shared) { - shell.rm('-rf', nestedCordovaLibPath); - } else if (!wasSymlink) { - // Delete only the src, since Eclipse / Android Studio can't handle their project files being deleted. - shell.rm('-rf', path.join(nestedCordovaLibPath, 'src')); - } - }); + fs.copySync(path.join(ROOT, 'cordova-js-src'), path.join(platform_www, 'cordova-js-src')); + if (shared) { var relativeFrameworkPath = path.relative(projectPath, getFrameworkDir(projectPath, true)); fs.symlinkSync(relativeFrameworkPath, nestedCordovaLibPath, 'dir'); } else { - shell.mkdir('-p', nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'build.gradle'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'cordova.gradle'), nestedCordovaLibPath); - shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath); + fs.ensureDirSync(nestedCordovaLibPath); + fs.copySync(path.join(ROOT, 'framework', 'AndroidManifest.xml'), path.join(nestedCordovaLibPath, 'AndroidManifest.xml')); + fs.copySync(path.join(ROOT, 'framework', 'project.properties'), path.join(nestedCordovaLibPath, 'project.properties')); + fs.copySync(path.join(ROOT, 'framework', 'build.gradle'), path.join(nestedCordovaLibPath, 'build.gradle')); + fs.copySync(path.join(ROOT, 'framework', 'cordova.gradle'), path.join(nestedCordovaLibPath, 'cordova.gradle')); + fs.copySync(path.join(ROOT, 'framework', 'src'), path.join(nestedCordovaLibPath, 'src')); } } @@ -150,12 +122,12 @@ function copyBuildRules (projectPath, isLegacy) { if (isLegacy) { // The project's build.gradle is identical to the earlier build.gradle, so it should still work - shell.cp('-f', path.join(srcDir, 'legacy', 'build.gradle'), projectPath); - shell.cp('-f', path.join(srcDir, 'wrapper.gradle'), projectPath); + fs.copySync(path.join(srcDir, 'legacy', 'build.gradle'), path.join(projectPath, 'legacy', 'build.gradle')); + fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle')); } else { - shell.cp('-f', path.join(srcDir, 'build.gradle'), projectPath); - shell.cp('-f', path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app')); - shell.cp('-f', path.join(srcDir, 'wrapper.gradle'), projectPath); + fs.copySync(path.join(srcDir, 'build.gradle'), path.join(projectPath, 'build.gradle')); + fs.copySync(path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app', 'build.gradle')); + fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle')); } } @@ -164,24 +136,29 @@ function copyScripts (projectPath) { var srcScriptsDir = path.join(bin, 'templates', 'cordova'); var destScriptsDir = path.join(projectPath, 'cordova'); // Delete old scripts directory if this is an update. - shell.rm('-rf', destScriptsDir); + fs.removeSync(destScriptsDir); // Copy in the new ones. - shell.cp('-r', srcScriptsDir, projectPath); + fs.copySync(srcScriptsDir, destScriptsDir); let nodeModulesDir = path.join(ROOT, 'node_modules'); - if (fs.existsSync(nodeModulesDir)) shell.cp('-r', nodeModulesDir, destScriptsDir); + if (fs.existsSync(nodeModulesDir)) fs.copySync(nodeModulesDir, path.join(destScriptsDir, 'node_modules')); + + fs.copySync(path.join(bin, 'check_reqs'), path.join(destScriptsDir, 'check_reqs')); + fs.copySync(path.join(bin, 'check_reqs.bat'), path.join(destScriptsDir, 'check_reqs.bat')); + fs.copySync(path.join(bin, 'android_sdk_version'), path.join(destScriptsDir, 'android_sdk_version')); + fs.copySync(path.join(bin, 'android_sdk_version.bat'), path.join(destScriptsDir, 'android_sdk_version.bat')); - shell.cp(path.join(bin, 'check_reqs*'), destScriptsDir); - shell.cp(path.join(bin, 'android_sdk_version*'), destScriptsDir); var check_reqs = path.join(destScriptsDir, 'check_reqs'); var android_sdk_version = path.join(destScriptsDir, 'android_sdk_version'); + // TODO: the two files being edited on-the-fly here are shared between - // platform and project-level commands. the below `sed` is updating the + // platform and project-level commands. the below is updating the // `require` path for the two libraries. if there's a better way to share // modules across both the repo and generated projects, we should make sure // to remove/update this. - shell.sed('-i', /templates\/cordova\//, '', android_sdk_version); - shell.sed('-i', /templates\/cordova\//, '', check_reqs); + let templatesCordovaRegex = /templates\/cordova\//; + utils.replaceFileContents(android_sdk_version, templatesCordovaRegex, ''); + utils.replaceFileContents(check_reqs, templatesCordovaRegex, ''); } /** @@ -273,51 +250,50 @@ exports.create = function (project_path, config, options, events) { events.emit('verbose', 'Copying android template project to ' + project_path); - exports.setShellFatal(true, function () { - var project_template_dir = options.customTemplate || path.join(ROOT, 'bin', 'templates', 'project'); - var app_path = path.join(project_path, 'app', 'src', 'main'); - - // copy project template - shell.mkdir('-p', app_path); - shell.cp('-r', path.join(project_template_dir, 'assets'), app_path); - shell.cp('-r', path.join(project_template_dir, 'res'), app_path); - shell.cp(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore')); - - // Manually create directories that would be empty within the template (since git doesn't track directories). - shell.mkdir(path.join(app_path, 'libs')); - - // copy cordova.js, cordova.jar - exports.copyJsAndLibrary(project_path, options.link, safe_activity_name); - - // Set up ther Android Studio paths - var java_path = path.join(app_path, 'java'); - var assets_path = path.join(app_path, 'assets'); - var resource_path = path.join(app_path, 'res'); - shell.mkdir('-p', java_path); - shell.mkdir('-p', assets_path); - shell.mkdir('-p', resource_path); - - // interpolate the activity name and package - var packagePath = package_name.replace(/\./g, path.sep); - var activity_dir = path.join(java_path, packagePath); - var activity_path = path.join(activity_dir, safe_activity_name + '.java'); - - shell.mkdir('-p', activity_dir); - shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path); - shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path); - shell.sed('-i', /__NAME__/, project_name, path.join(app_path, 'res', 'values', 'strings.xml')); - shell.sed('-i', /__ID__/, package_name, activity_path); - - var manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml')); - manifest.setPackageId(package_name) - .getActivity().setName(safe_activity_name); - - var manifest_path = path.join(app_path, 'AndroidManifest.xml'); - manifest.write(manifest_path); - - exports.copyScripts(project_path); - exports.copyBuildRules(project_path); - }); + var project_template_dir = options.customTemplate || path.join(ROOT, 'bin', 'templates', 'project'); + var app_path = path.join(project_path, 'app', 'src', 'main'); + + // copy project template + fs.ensureDirSync(app_path); + fs.copySync(path.join(project_template_dir, 'assets'), path.join(app_path, 'assets')); + fs.copySync(path.join(project_template_dir, 'res'), path.join(app_path, 'res')); + fs.copySync(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore')); + + // Manually create directories that would be empty within the template (since git doesn't track directories). + fs.ensureDirSync(path.join(app_path, 'libs')); + + // copy cordova.js, cordova.jar + exports.copyJsAndLibrary(project_path, options.link, safe_activity_name); + + // Set up ther Android Studio paths + var java_path = path.join(app_path, 'java'); + var assets_path = path.join(app_path, 'assets'); + var resource_path = path.join(app_path, 'res'); + fs.ensureDirSync(java_path); + fs.ensureDirSync(assets_path); + fs.ensureDirSync(resource_path); + + // interpolate the activity name and package + var packagePath = package_name.replace(/\./g, path.sep); + var activity_dir = path.join(java_path, packagePath); + var activity_path = path.join(activity_dir, safe_activity_name + '.java'); + + fs.ensureDirSync(activity_dir); + fs.copySync(path.join(project_template_dir, 'Activity.java'), activity_path); + utils.replaceFileContents(activity_path, /__ACTIVITY__/, safe_activity_name); + utils.replaceFileContents(path.join(app_path, 'res', 'values', 'strings.xml'), /__NAME__/, project_name); + utils.replaceFileContents(activity_path, /__ID__/, package_name); + + var manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml')); + manifest.setPackageId(package_name) + .getActivity().setName(safe_activity_name); + + var manifest_path = path.join(app_path, 'AndroidManifest.xml'); + manifest.write(manifest_path); + + exports.copyScripts(project_path); + exports.copyBuildRules(project_path); + // Link it to local android install. exports.writeProjectProperties(project_path, target_api); exports.prepBuildFiles(project_path); diff --git a/bin/lib/utils.js b/bin/lib/utils.js new file mode 100644 index 0000000000..71925e81eb --- /dev/null +++ b/bin/lib/utils.js @@ -0,0 +1,47 @@ +/* + 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. +*/ + +/* + Provides a set of utility methods, which can also be spied on during unit tests. +*/ + +// TODO: Perhaps this should live in cordova-common? + +const fs = require('fs-extra'); + +/** + * Reads, searches, and replaces the found occurences with replacementString and then writes the file back out. + * A backup is not made. + * + * @param {string} file A file path to a readable & writable file + * @param {RegExp} searchRegex The search regex + * @param {string} replacementString The string to replace the found occurences + * @returns {void} + */ +exports.replaceFileContents = function (file, searchRegex, replacementString) { + // let contents; + try { + var contents = fs.readFileSync(file).toString(); + } catch (ex) { + console.log('TRYING TO READ: ', file); + throw ex; + } + contents = contents.replace(searchRegex, replacementString); + fs.writeFileSync(file, contents); +}; diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js index 6123498303..1196634976 100644 --- a/bin/templates/cordova/lib/build.js +++ b/bin/templates/cordova/lib/build.js @@ -264,45 +264,23 @@ module.exports.findBestApkForArchitecture = function (buildResults, arch) { }; function PackageInfo (keystore, alias, storePassword, password, keystoreType) { - this.keystore = { - 'name': 'key.store', - 'value': keystore - }; - this.alias = { - 'name': 'key.alias', - 'value': alias - }; - if (storePassword) { - this.storePassword = { - 'name': 'key.store.password', - 'value': storePassword - }; - } - if (password) { - this.password = { - 'name': 'key.alias.password', - 'value': password - }; - } - if (keystoreType) { - this.keystoreType = { - 'name': 'key.store.type', - 'value': keystoreType - }; - } + const createNameKeyObject = (name, value) => ({ name, value: value.replace(/\\/g, '\\\\') }); + + this.data = [ + createNameKeyObject('key.store', keystore), + createNameKeyObject('key.alias', alias) + ]; + + if (storePassword) this.data.push(createNameKeyObject('key.store.password', storePassword)); + if (password) this.data.push(createNameKeyObject('key.alias.password', password)); + if (keystoreType) this.data.push(createNameKeyObject('key.store.type', keystoreType)); } PackageInfo.prototype = { - toProperties: function () { - var self = this; - var result = ''; - Object.keys(self).forEach(function (key) { - result += self[key].name; - result += '='; - result += self[key].value.replace(/\\/g, '\\\\'); - result += '\n'; - }); - return result; + appendToProperties: function (propertiesParser) { + for (const { name, value } of this.data) propertiesParser.set(name, value); + + propertiesParser.save(); } }; diff --git a/bin/templates/cordova/lib/builders/ProjectBuilder.js b/bin/templates/cordova/lib/builders/ProjectBuilder.js index df05e495f2..79ae8152b8 100644 --- a/bin/templates/cordova/lib/builders/ProjectBuilder.js +++ b/bin/templates/cordova/lib/builders/ProjectBuilder.js @@ -17,15 +17,15 @@ under the License. */ -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); -var shell = require('shelljs'); const execa = require('execa'); var events = require('cordova-common').events; var CordovaError = require('cordova-common').CordovaError; var check_reqs = require('../check_reqs'); var PackageType = require('../PackageType'); const compareFunc = require('compare-func'); +const { createEditor } = require('properties-parser'); const MARKER = 'YOUR CHANGES WILL BE ERASED!'; const SIGNING_PROPERTIES = '-signing.properties'; @@ -33,6 +33,67 @@ const TEMPLATE = '# This file is automatically generated.\n' + '# Do not modify this file -- ' + MARKER + '\n'; +const fileSorter = compareFunc([ + // Sort arch specific builds after generic ones + filePath => /-x86|-arm/.test(filePath), + + // Sort unsigned builds after signed ones + filePath => /-unsigned/.test(filePath), + + // Sort by file modification time, latest first + filePath => -fs.statSync(filePath).mtime.getTime(), + + // Sort by file name length, ascending + 'length' +]); + +/** + * If the provided directory does not exist or extension is missing, return an empty array. + * If the director exists, loop the directories and collect list of files matching the extension. + * + * @param {String} dir Directory to scan + * @param {String} extension + */ +function recursivelyFindFiles (dir, extension) { + if (!fs.existsSync(dir) || !extension) return []; + + const files = fs.readdirSync(dir, { withFileTypes: true }) + .map(entry => { + const item = path.resolve(dir, entry.name); + + if (entry.isDirectory()) return recursivelyFindFiles(item, extension); + if (path.extname(entry.name) === `.${extension}`) return item; + return false; + }); + + return Array.prototype.concat(...files) + .filter(file => file !== false); +} + +/** + * @param {String} dir + * @param {String} build_type + * @param {String} arch + * @param {String} extension + */ +function findOutputFilesHelper (dir, build_type, arch, extension) { + let files = recursivelyFindFiles(path.resolve(dir, build_type), extension); + + if (files.length === 0) return files; + + // Assume arch-specific build if newest apk has -x86 or -arm. + let archSpecific = !!/-x86|-arm/.exec(path.basename(files[0])); + + // And show only arch-specific ones (or non-arch-specific) + files = files.filter(p => !!/-x86|-arm/.exec(path.basename(p)) === archSpecific); + + if (archSpecific && files.length > 1 && arch) { + files = files.filter(p => path.basename(p).indexOf('-' + arch) !== -1); + } + + return files; +} + class ProjectBuilder { constructor (rootDirectory) { this.root = rootDirectory || path.resolve(__dirname, '../../..'); @@ -131,7 +192,7 @@ class ProjectBuilder { try { fs.accessSync(subProjectGradle, fs.F_OK); } catch (e) { - shell.cp('-f', pluginBuildGradle, subProjectGradle); + fs.copySync(pluginBuildGradle, subProjectGradle); } }; @@ -221,22 +282,25 @@ class ProjectBuilder { return self.runGradleWrapper(gradlePath); }).then(function () { return self.prepBuildFiles(); - }).then(function () { - // If the gradle distribution URL is set, make sure it points to version we want. - // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with. - // For some reason, using ^ and $ don't work. This does the job, though. - var distributionUrlRegex = /distributionUrl.*zip/; - var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-6.1-all.zip'; - var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties'); - shell.chmod('u+w', gradleWrapperPropertiesPath); - shell.sed('-i', distributionUrlRegex, 'distributionUrl=' + distributionUrl, gradleWrapperPropertiesPath); - - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(self.root, propertiesFile); + }).then(() => { + // update/set the distributionUrl in the gradle-wrapper.properties + const gradleWrapperPropertiesPath = path.join(self.root, 'gradle/wrapper/gradle-wrapper.properties'); + const gradleWrapperProperties = createEditor(gradleWrapperPropertiesPath); + const distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https://services.gradle.org/distributions/gradle-6.1-all.zip'; + gradleWrapperProperties.set('distributionUrl', distributionUrl); + gradleWrapperProperties.save(); + + events.emit('verbose', `Gradle Distribution URL: ${distributionUrl}`); + }) + .then(() => { + const signingPropertiesPath = path.join(self.root, `${opts.buildType}${SIGNING_PROPERTIES}`); + + if (fs.existsSync(signingPropertiesPath)) fs.removeSync(signingPropertiesPath); if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if (isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); + fs.ensureFileSync(signingPropertiesPath); + const signingProperties = createEditor(signingPropertiesPath); + signingProperties.addHeadComment(TEMPLATE); + opts.packageInfo.appendToProperties(signingProperties); } }); } @@ -263,28 +327,29 @@ class ProjectBuilder { } clean (opts) { - var builder = this; - var wrapper = path.join(this.root, 'gradlew'); - var args = builder.getArgs('clean', opts); + const wrapper = path.join(this.root, 'gradlew'); + const args = this.getArgs('clean', opts); return execa(wrapper, args, { stdio: 'inherit' }) - .then(function () { - shell.rm('-rf', path.join(builder.root, 'out')); + .then(() => { + fs.removeSync(path.join(this.root, 'out')); - ['debug', 'release'].forEach(function (config) { - var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES); - if (isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); + ['debug', 'release'].map(config => path.join(this.root, `${config}${SIGNING_PROPERTIES}`)) + .forEach(file => { + const hasFile = fs.existsSync(file); + const hasMarker = hasFile && fs.readFileSync(file, 'utf8') + .includes(MARKER); + + if (hasFile && hasMarker) fs.removeSync(file); + }); }); } findOutputApks (build_type, arch) { - return findOutputApksHelper(this.apkDir, build_type, arch).sort(apkSorter); + return findOutputFilesHelper(this.apkDir, build_type, arch, 'apk').sort(fileSorter); } findOutputBundles (build_type) { - return findOutputBundlesHelper(this.aabDir, build_type); + return findOutputFilesHelper(this.aabDir, build_type, false, 'aab').sort(fileSorter); } fetchBuildResults (build_type, arch) { @@ -296,93 +361,3 @@ class ProjectBuilder { } module.exports = ProjectBuilder; - -const apkSorter = compareFunc([ - // Sort arch specific builds after generic ones - apkPath => /-x86|-arm/.test(apkPath), - - // Sort unsigned builds after signed ones - apkPath => /-unsigned/.test(apkPath), - - // Sort by file modification time, latest first - apkPath => -fs.statSync(apkPath).mtime.getTime(), - - // Sort by file name length, ascending - 'length' -]); - -function findOutputApksHelper (dir, build_type, arch) { - var shellSilent = shell.config.silent; - shell.config.silent = true; - - // list directory recursively - var ret = shell.ls('-R', dir).map(function (file) { - // ls does not include base directory - return path.join(dir, file); - }).filter(function (file) { - // find all APKs - return file.match(/\.apk?$/i); - }).filter(function (candidate) { - var apkName = path.basename(candidate); - // Need to choose between release and debug .apk. - if (build_type === 'debug') { - return /-debug/.exec(apkName) && !/-unaligned|-unsigned/.exec(apkName); - } - if (build_type === 'release') { - return /-release/.exec(apkName) && !/-unaligned/.exec(apkName); - } - return true; - }).sort(apkSorter); - - shell.config.silent = shellSilent; - - if (ret.length === 0) { - return ret; - } - // Assume arch-specific build if newest apk has -x86 or -arm. - var archSpecific = !!/-x86|-arm/.exec(path.basename(ret[0])); - // And show only arch-specific ones (or non-arch-specific) - ret = ret.filter(function (p) { - return !!/-x86|-arm/.exec(path.basename(p)) === archSpecific; - }); - - if (archSpecific && ret.length > 1 && arch) { - ret = ret.filter(function (p) { - return path.basename(p).indexOf('-' + arch) !== -1; - }); - } - - return ret; -} - -// This method was a copy of findOutputApksHelper and modified to look for bundles -// While replacing shell with fs-extra, it might be a good idea to see if we can -// generalise these findOutput methods. -function findOutputBundlesHelper (dir, build_type) { - const shellSilent = shell.config.silent; - shell.config.silent = true; - - // list directory recursively - const ret = shell.ls('-R', dir).map(function (file) { - return path.join(dir, file); // ls does not include base directory - }).filter(function (file) { - return file.match(/\.aab?$/i); // find all bundles - }).filter(function (candidate) { - // Need to choose between release and debug bundle. - if (build_type === 'debug') { - return /debug/.exec(candidate); - } - if (build_type === 'release') { - return /release/.exec(candidate); - } - return true; - }); - - shell.config.silent = shellSilent; - - return ret; -} - -function isAutoGenerated (file) { - return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; -} diff --git a/bin/templates/cordova/lib/check_reqs.js b/bin/templates/cordova/lib/check_reqs.js index de0a9eaa92..b139b3d011 100644 --- a/bin/templates/cordova/lib/check_reqs.js +++ b/bin/templates/cordova/lib/check_reqs.js @@ -20,10 +20,10 @@ */ const execa = require('execa'); -var shelljs = require('shelljs'); var path = require('path'); -var fs = require('fs'); +var fs = require('fs-extra'); var os = require('os'); +var which = require('which'); var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..'); var PROJECT_ROOT = path.join(__dirname, '..', '..'); const { CordovaError, ConfigParser, events } = require('cordova-common'); @@ -31,11 +31,25 @@ var android_sdk = require('./android_sdk'); const { createEditor } = require('properties-parser'); function forgivingWhichSync (cmd) { - try { - return fs.realpathSync(shelljs.which(cmd)); - } catch (e) { - return ''; + let whichResult = which.sync(cmd, { nothrow: true }); + + // On null, returns empty string to maintain backwards compatibility + // realpathSync follows symlinks + return whichResult === null ? '' : fs.realpathSync(whichResult); +} + +function getJDKDirectory (directory) { + let p = path.resolve(directory, 'java'); + if (fs.existsSync(p)) { + let directories = fs.readdirSync(p); + for (let i = 0; i < directories.length; i++) { + let dir = directories[i]; + if (/^(jdk)+./.test(dir)) { + return path.resolve(directory, 'java', dir); + } + } } + return null; } module.exports.isWindows = function () { @@ -182,7 +196,6 @@ module.exports.check_java = function () { }); } else { // See if we can derive it from javac's location. - // fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK var maybeJavaHome = path.dirname(path.dirname(javacPath)); if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { process.env['JAVA_HOME'] = maybeJavaHome; @@ -191,16 +204,16 @@ module.exports.check_java = function () { } } } else if (module.exports.isWindows()) { - // Try to auto-detect java in the default install paths. - var oldSilent = shelljs.config.silent; - shelljs.config.silent = true; - var firstJdkDir = - shelljs.ls(process.env['ProgramFiles'] + '\\java\\jdk*')[0] || - shelljs.ls('C:\\Program Files\\java\\jdk*')[0] || - shelljs.ls('C:\\Program Files (x86)\\java\\jdk*')[0]; - shelljs.config.silent = oldSilent; + const programFilesEnv = path.resolve(process.env['ProgramFiles']); + const programFiles = 'C:\\Program Files\\'; + const programFilesx86 = 'C:\\Program Files (x86)\\'; + + let firstJdkDir = + getJDKDirectory(programFilesEnv) || + getJDKDirectory(programFiles) || + getJDKDirectory(programFilesx86); + if (firstJdkDir) { - // shelljs always uses / in paths. firstJdkDir = firstJdkDir.replace(/\//g, path.sep); if (!javacPath) { process.env['PATH'] += path.delimiter + path.join(firstJdkDir, 'bin'); diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index b5750520b2..3c339c29f9 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -20,6 +20,7 @@ */ const execa = require('execa'); +const fs = require('fs-extra'); var android_versions = require('android-versions'); var retry = require('./retry'); var build = require('./build'); @@ -28,27 +29,25 @@ var Adb = require('./Adb'); var AndroidManifest = require('./AndroidManifest'); var events = require('cordova-common').events; var CordovaError = require('cordova-common').CordovaError; -var shelljs = require('shelljs'); var android_sdk = require('./android_sdk'); var check_reqs = require('./check_reqs'); - +var which = require('which'); var os = require('os'); -var fs = require('fs'); // constants -var ONE_SECOND = 1000; // in milliseconds -var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds -var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds -var NUM_INSTALL_RETRIES = 3; -var CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds -var EXEC_KILL_SIGNAL = 'SIGKILL'; +const ONE_SECOND = 1000; // in milliseconds +const ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds +const INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds +const NUM_INSTALL_RETRIES = 3; +const CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds +const EXEC_KILL_SIGNAL = 'SIGKILL'; function forgivingWhichSync (cmd) { - try { - return fs.realpathSync(shelljs.which(cmd)); - } catch (e) { - return ''; - } + let whichResult = which.sync(cmd, { nothrow: true }); + + // On null, returns empty string to maintain backwards compatibility + // realpathSync follows symlinks + return whichResult === null ? '' : fs.realpathSync(whichResult); } module.exports.list_images_using_avdmanager = function () { @@ -290,7 +289,7 @@ module.exports.start = function (emulator_ID, boot_timeout) { return self.get_available_port().then(function (port) { // Figure out the directory the emulator binary runs in, and set the cwd to that directory. // Workaround for https://code.google.com/p/android/issues/detail?id=235461 - var emulator_dir = path.dirname(shelljs.which('emulator')); + var emulator_dir = path.dirname(which.sync('emulator')); var args = ['-avd', emulatorId, '-port', port]; // Don't wait for it to finish, since the emulator will probably keep running for a long time. execa('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir }) diff --git a/bin/templates/cordova/lib/pluginHandlers.js b/bin/templates/cordova/lib/pluginHandlers.js index 6d1a7336e3..d0de51385d 100644 --- a/bin/templates/cordova/lib/pluginHandlers.js +++ b/bin/templates/cordova/lib/pluginHandlers.js @@ -14,9 +14,8 @@ * */ -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); -var shell = require('shelljs'); var events = require('cordova-common').events; var CordovaError = require('cordova-common').CordovaError; @@ -42,7 +41,7 @@ var handlers = { deleteJava(project.projectDir, dest); } else { // Just remove the file, not the whole parent directory - removeFile(project.projectDir, dest); + removeFile(path.resolve(project.projectDir, dest)); } } }, @@ -53,7 +52,7 @@ var handlers = { }, uninstall: function (obj, plugin, project, options) { var dest = path.join('app/libs', path.basename(obj.src)); - removeFile(project.projectDir, dest); + removeFile(path.resolve(project.projectDir, dest)); } }, 'resource-file': { @@ -63,7 +62,7 @@ var handlers = { }, uninstall: function (obj, plugin, project, options) { var dest = path.join('app', 'src', 'main', obj.target); - removeFile(project.projectDir, dest); + removeFile(path.resolve(project.projectDir, dest)); } }, 'framework': { @@ -102,7 +101,7 @@ var handlers = { if (obj.custom) { var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src); - removeFile(project.projectDir, subRelativeDir); + removeFile(path.resolve(project.projectDir, subRelativeDir)); subDir = path.resolve(project.projectDir, subRelativeDir); // If it's the last framework in the plugin, remove the parent directory. var parDir = path.dirname(subDir); @@ -143,12 +142,12 @@ var handlers = { if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id)); - removeFileF(path.resolve(project.www, target)); - removeFileF(path.resolve(project.www, 'plugins', plugin.id)); + removeFile(path.resolve(project.www, target)); + removeFile(path.resolve(project.www, 'plugins', plugin.id)); if (options && options.usePlatformWww) { // CB-11022 remove file from both directories if usePlatformWww is specified - removeFileF(path.resolve(project.platformWww, target)); - removeFileF(path.resolve(project.platformWww, 'plugins', plugin.id)); + removeFile(path.resolve(project.platformWww, target)); + removeFile(path.resolve(project.platformWww, 'plugins', plugin.id)); } } }, @@ -166,13 +165,13 @@ var handlers = { scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n'; var wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src); - shell.mkdir('-p', path.dirname(wwwDest)); + fs.ensureDirSync(path.dirname(wwwDest)); fs.writeFileSync(wwwDest, scriptContent, 'utf-8'); if (options && options.usePlatformWww) { // CB-11022 copy file to both directories if usePlatformWww is specified var platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src); - shell.mkdir('-p', path.dirname(platformWwwDest)); + fs.ensureDirSync(path.dirname(platformWwwDest)); fs.writeFileSync(platformWwwDest, scriptContent, 'utf-8'); } }, @@ -217,14 +216,11 @@ function copyFile (plugin_dir, src, project_dir, dest, link) { // check that dest path is located in project directory if (dest.indexOf(project_dir) !== 0) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); } - shell.mkdir('-p', path.dirname(dest)); + fs.ensureDirSync(path.dirname(dest)); if (link) { symlinkFileOrDirTree(src, dest); - } else if (fs.statSync(src).isDirectory()) { - // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq - shell.cp('-Rf', src + '/*', dest); } else { - shell.cp('-f', src, dest); + fs.copySync(src, dest); } } @@ -238,11 +234,11 @@ function copyNewFile (plugin_dir, src, project_dir, dest, link) { function symlinkFileOrDirTree (src, dest) { if (fs.existsSync(dest)) { - shell.rm('-Rf', dest); + fs.removeSync(dest); } if (fs.statSync(src).isDirectory()) { - shell.mkdir('-p', dest); + fs.ensureDirSync(path.dirname(dest)); fs.readdirSync(src).forEach(function (entry) { symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry)); }); @@ -251,15 +247,8 @@ function symlinkFileOrDirTree (src, dest) { } } -// checks if file exists and then deletes. Error if doesn't exist -function removeFile (project_dir, src) { - var file = path.resolve(project_dir, src); - shell.rm('-Rf', file); -} - -// deletes file/directory without checking -function removeFileF (file) { - shell.rm('-Rf', file); +function removeFile (file) { + fs.removeSync(file); } // Sometimes we want to remove some java, and prune any unnecessary empty directories @@ -272,7 +261,7 @@ function removeFileAndParents (baseDir, destFile, stopper) { var file = path.resolve(baseDir, destFile); if (!fs.existsSync(file)) return; - removeFileF(file); + removeFile(file); // check if directory is empty var curDir = path.dirname(file); diff --git a/bin/templates/cordova/lib/prepare.js b/bin/templates/cordova/lib/prepare.js index 14abcee495..f8cc54c856 100644 --- a/bin/templates/cordova/lib/prepare.js +++ b/bin/templates/cordova/lib/prepare.js @@ -17,9 +17,8 @@ under the License. */ -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); -var shell = require('shelljs'); var events = require('cordova-common').events; var AndroidManifest = require('./AndroidManifest'); var checkReqs = require('./check_reqs'); @@ -30,6 +29,7 @@ var FileUpdater = require('cordova-common').FileUpdater; var PlatformJson = require('cordova-common').PlatformJson; var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger; var PluginInfoProvider = require('cordova-common').PluginInfoProvider; +let utils = require('./utils'); const GradlePropertiesParser = require('./config/GradlePropertiesParser'); @@ -120,7 +120,7 @@ function updateConfigFilesFrom (sourceConfig, configMunger, locations) { // First cleanup current config and merge project's one into own // Overwrite platform config.xml with defaults.xml. - shell.cp('-f', locations.defaultConfigXml, locations.configXml); + fs.copySync(locations.defaultConfigXml, locations.configXml); // Then apply config changes from global munge to all config files // in project (including project's config) @@ -222,9 +222,10 @@ function updateProjectAccordingTo (platformConfig, locations) { .write(); // Java file paths shouldn't be hard coded - var javaPattern = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'), '*.java'); - var java_files = shell.ls(javaPattern).filter(function (f) { - return shell.grep(/extends\s+CordovaActivity/g, f); + let javaDirectory = path.join(locations.javaSrc, manifestId.replace(/\./g, '/')); + let javaPattern = /\.java$/; + let java_files = utils.scanDirectory(javaDirectory, javaPattern, true).filter(function (f) { + return utils.grep(f, /extends\s+CordovaActivity/g) !== null; }); if (java_files.length === 0) { @@ -233,9 +234,15 @@ function updateProjectAccordingTo (platformConfig, locations) { events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]); } - var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0])); - shell.mkdir('-p', path.dirname(destFile)); - shell.sed(/package [\w.]*;/, 'package ' + androidPkgName + ';', java_files[0]).to(destFile); + let destFile = java_files[0]; + + // var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0])); + // fs.ensureDirSync(path.dirname(destFile)); + // events.emit('verbose', java_files[0]); + // events.emit('verbose', destFile); + // console.log(locations); + // fs.copySync(java_files[0], destFile); + utils.replaceFileContents(destFile, /package [\w.]*;/, 'package ' + androidPkgName + ';'); events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile); var removeOrigPkg = checkReqs.isWindows() || checkReqs.isDarwin() ? @@ -244,7 +251,7 @@ function updateProjectAccordingTo (platformConfig, locations) { if (removeOrigPkg) { // If package was name changed we need to remove old java with main activity - shell.rm('-Rf', java_files[0]); + fs.removeSync(java_files[0]); // remove any empty directories var currentDir = path.dirname(java_files[0]); var sourcesRoot = path.resolve(locations.root, 'src'); @@ -637,9 +644,10 @@ function cleanIcons (projectRoot, projectConfig, platformResourcesDir) { * Gets a map containing resources of a specified name from all drawable folders in a directory. */ function mapImageResources (rootDir, subDir, type, resourceName) { - var pathMap = {}; - shell.ls(path.join(rootDir, subDir, type + '-*')).forEach(function (drawableFolder) { - var imagePath = path.join(subDir, path.basename(drawableFolder), resourceName); + let pathMap = {}; + let pattern = new RegExp(type + '+-.+'); + utils.scanDirectory(path.join(rootDir, subDir), pattern).forEach(function (drawableFolder) { + let imagePath = path.join(subDir, path.basename(drawableFolder), resourceName); pathMap[imagePath] = null; }); return pathMap; diff --git a/bin/templates/cordova/lib/utils.js b/bin/templates/cordova/lib/utils.js new file mode 100644 index 0000000000..5772e36a92 --- /dev/null +++ b/bin/templates/cordova/lib/utils.js @@ -0,0 +1,97 @@ +/* + 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. +*/ + +/* + Provides a set of utility methods, which can also be spied on during unit tests. +*/ + +// TODO: Perhaps this should live in cordova-common? + +const fs = require('fs-extra'); +const path = require('path'); + +/** + * Reads, searches, and replaces the found occurences with replacementString and then writes the file back out. + * A backup is not made. + * + * @param {string} file A file path to a readable & writable file + * @param {RegExp} searchRegex The search regex + * @param {string} replacementString The string to replace the found occurences + * @returns void + */ +exports.replaceFileContents = function (file, searchRegex, replacementString) { + let contents = fs.readFileSync(file).toString(); + contents = contents.replace(searchRegex, replacementString); + fs.writeFileSync(file, contents); +}; + +/** + * Reads a file and scans for regex. Returns the line of the first occurence or null if no occurences are found. + * + * @param {string} file A file path + * @param {RegExp} regex A search regex + * @returns string|null + */ +exports.grep = function (file, regex) { + let contents = fs.readFileSync(file).toString().replace(/\\r/g, '').split('\n'); + for (let i = 0; i < contents.length; i++) { + let line = contents[i]; + if (regex.test(line)) { + return line; + } + } + return null; +}; + +/** + * Scans directories and outputs a list of found paths that matches the regex + * + * @param {string} directory The starting directory + * @param {RegExp} regex The search regex + * @param {boolean} recursive Enables recursion + * @returns Array + */ +exports.scanDirectory = function (directory, regex, recursive) { + let output = []; + + if (fs.existsSync(directory)) { + let items = fs.readdirSync(directory); + + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let itemPath = path.join(directory, item); + let stats = fs.statSync(itemPath); + + if (regex.test(itemPath)) { + output.push(itemPath); + } + + if (stats.isDirectory()) { + if (recursive) { + output = output.concat(exports.scanDirectory(itemPath, regex, recursive)); + } else { + // Move onto the next item + continue; + } + } + } + } + + return output; +}; diff --git a/framework/.classpath b/framework/.classpath index 0461652ecf..eb19361b57 100644 --- a/framework/.classpath +++ b/framework/.classpath @@ -1,9 +1,6 @@ - - - - - - + + + diff --git a/package.json b/package.json index e7058f9e72..6aa8e966f6 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,10 @@ "compare-func": "^1.3.2", "cordova-common": "^3.2.0", "execa": "^3.2.0", + "fs-extra": "^8.1.0", "nopt": "^4.0.1", "properties-parser": "^0.3.1", - "shelljs": "^0.5.3" + "which": "^1.3.1" }, "devDependencies": { "eslint": "^5.12.0", diff --git a/spec/e2e/helpers/projectActions.js b/spec/e2e/helpers/projectActions.js new file mode 100644 index 0000000000..63aab73bd6 --- /dev/null +++ b/spec/e2e/helpers/projectActions.js @@ -0,0 +1,150 @@ +/* + 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 PluginInfoProvider = require('cordova-common').PluginInfoProvider; +let fs = require('fs-extra'); +var cp = require('child_process'); +var path = require('path'); +var util = require('util'); + +var cordova_bin = path.join(__dirname, '../../../bin'); + +/** + * Creates a project using platform create script with given parameters + * @param {string} projectname - name of the project + * @param {string} projectid - id of the project + * @param {string} platformpath - path to the platform + * @param {function} callback - function which is called (without arguments) when the project is created or (with error object) when error occurs + */ +module.exports.createProject = function (projectname, projectid, platformpath, callback) { + // platformpath is optional + if (!callback && typeof platformpath === 'function') { + callback = platformpath; + platformpath = null; + } + var projectDirName = getDirName(projectid); + var createScriptPath = platformpath ? path.join(platformpath, 'bin/create') : path.join(cordova_bin, 'create'); + + // remove existing folder + module.exports.removeProject(projectid); + + // create the project + var command = util.format('"%s" %s %s "%s"', createScriptPath, projectDirName, projectid, projectname); + cp.exec(command, function (error, stdout, stderr) { + if (error) { + console.log(stdout); + console.error(stderr); + } + callback(error); + }); +}; + +/** + * Updates a project using platform update script with given parameters + * @param {string} projectid - id of the project + * @param {string} platformpath - path to the platform + * @param {function} callback - function which is called (without arguments) when the project is updated or (with error object) when error occurs + */ +module.exports.updateProject = function (projectid, platformpath, callback) { + // platformpath is optional + if (!callback && typeof platformpath === 'function') { + callback = platformpath; + platformpath = null; + } + var projectDirName = getDirName(projectid); + var updateScriptPath = platformpath ? path.join(platformpath, 'bin/update') : path.join(cordova_bin, 'update'); + var command = util.format('"%s" %s', updateScriptPath, projectDirName); + cp.exec(command, function (error, stdout, stderr) { + if (error) { + console.log(stdout); + console.error(stderr); + } + callback(error); + }); + +}; + +/** + * Builds a project using platform build script with given parameters + * @param {string} projectid - id of the project + * @param {function} callback - function which is called (without arguments) when the project is built or (with error object) when error occurs + */ +module.exports.buildProject = function (projectid, callback) { + var projectDirName = getDirName(projectid); + var command = path.join(projectDirName, 'cordova/build'); + + cp.exec(command, function (error, stdout, stderr) { + if (error) { + console.log(stdout); + console.error(stderr); + } + callback(error); + }); +}; + +/** + * Removes a project + * @param {string} projectid - id of the project + */ +module.exports.removeProject = function (projectid) { + var projectDirName = getDirName(projectid); + fs.removeSync(projectDirName); +}; + +/** + * Add a plugin to a project using platform api + * @param {string} projectid - id of the project + * @param {string} plugindir - path to a plugin + * @param {function} callback - function which is called (without arguments) when the plugin is added or (with error object) when error occurs + */ +module.exports.addPlugin = function (projectid, plugindir, callback) { + var projectDirName = getDirName(projectid); + var pip = new PluginInfoProvider(); + var pluginInfo = pip.get(plugindir); + var Api = require(path.join(__dirname, '../../..', projectDirName, 'cordova', 'Api.js')); + var api = new Api('android', projectDirName); + + api.addPlugin(pluginInfo).then(function () { + callback(null); + }, function (error) { + console.error(error); + callback(error); + }); +}; + +/** + * Gets a version number from project using platform script + * @param {string} projectid - id of the project + * @param {function} callback - function which is called with platform version as an argument + */ +module.exports.getPlatformVersion = function (projectid, callback) { + var command = path.join(getDirName(projectid), 'cordova/version'); + + cp.exec(command, function (error, stdout, stderr) { + if (error) { + console.log(stdout); + console.error(stderr); + } + callback(stdout.trim()); + }); +}; + +function getDirName (projectid) { + return 'test-' + projectid; +} diff --git a/spec/e2e/plugin.spec.js b/spec/e2e/plugin.spec.js index 30fc7b6fd5..ce56ce20f8 100644 --- a/spec/e2e/plugin.spec.js +++ b/spec/e2e/plugin.spec.js @@ -18,9 +18,8 @@ */ const os = require('os'); -const fs = require('fs'); +const fs = require('fs-extra'); const path = require('path'); -const shell = require('shelljs'); const execa = require('execa'); const { PluginInfoProvider } = require('cordova-common'); @@ -34,7 +33,7 @@ describe('plugin add', function () { tmpDir = fs.realpathSync(fs.mkdtempSync(tmpDirTemplate)); }); afterEach(() => { - shell.rm('-rf', tmpDir); + fs.removeSync(tmpDir); }); it('Test#001 : create project and add a plugin with framework', function () { diff --git a/spec/unit/builders/ProjectBuilder.spec.js b/spec/unit/builders/ProjectBuilder.spec.js index bf88025d18..5050c9a31e 100644 --- a/spec/unit/builders/ProjectBuilder.spec.js +++ b/spec/unit/builders/ProjectBuilder.spec.js @@ -17,7 +17,7 @@ under the License. */ -const fs = require('fs'); +const fs = require('fs-extra'); const path = require('path'); const rewire = require('rewire'); @@ -215,20 +215,20 @@ describe('ProjectBuilder', () => { }); describe('clean', () => { - let shellSpy; - beforeEach(() => { - shellSpy = jasmine.createSpyObj('shell', ['rm']); - ProjectBuilder.__set__('shell', shellSpy); + const marker = ProjectBuilder.__get__('MARKER'); + spyOn(fs, 'readFileSync').and.returnValue(`Some Header Here: ${marker}`); + spyOn(fs, 'removeSync'); spyOn(builder, 'getArgs'); execaSpy.and.returnValue(Promise.resolve()); }); it('should get arguments for cleaning', () => { const opts = {}; - builder.clean(opts); - expect(builder.getArgs).toHaveBeenCalledWith('clean', opts); + return builder.clean(opts).then(() => { + expect(builder.getArgs).toHaveBeenCalledWith('clean', opts); + }); }); it('should spawn gradle', () => { @@ -243,7 +243,7 @@ describe('ProjectBuilder', () => { it('should remove "out" folder', () => { return builder.clean({}).then(() => { - expect(shellSpy.rm).toHaveBeenCalledWith('-rf', path.join(rootDir, 'out')); + expect(fs.removeSync).toHaveBeenCalledWith(path.join(rootDir, 'out')); }); }); @@ -251,13 +251,11 @@ describe('ProjectBuilder', () => { const debugSigningFile = path.join(rootDir, 'debug-signing.properties'); const releaseSigningFile = path.join(rootDir, 'release-signing.properties'); - const isAutoGeneratedSpy = jasmine.createSpy('isAutoGenerated'); - ProjectBuilder.__set__('isAutoGenerated', isAutoGeneratedSpy); - isAutoGeneratedSpy.and.returnValue(true); + spyOn(fs, 'existsSync').and.returnValue(true); return builder.clean({}).then(() => { - expect(shellSpy.rm).toHaveBeenCalledWith(jasmine.any(String), debugSigningFile); - expect(shellSpy.rm).toHaveBeenCalledWith(jasmine.any(String), releaseSigningFile); + expect(fs.removeSync).toHaveBeenCalledWith(debugSigningFile); + expect(fs.removeSync).toHaveBeenCalledWith(releaseSigningFile); }); }); @@ -265,18 +263,16 @@ describe('ProjectBuilder', () => { const debugSigningFile = path.join(rootDir, 'debug-signing.properties'); const releaseSigningFile = path.join(rootDir, 'release-signing.properties'); - const isAutoGeneratedSpy = jasmine.createSpy('isAutoGenerated'); - ProjectBuilder.__set__('isAutoGenerated', isAutoGeneratedSpy); - isAutoGeneratedSpy.and.returnValue(false); + spyOn(fs, 'existsSync').and.returnValue(false); return builder.clean({}).then(() => { - expect(shellSpy.rm).not.toHaveBeenCalledWith(jasmine.any(String), debugSigningFile); - expect(shellSpy.rm).not.toHaveBeenCalledWith(jasmine.any(String), releaseSigningFile); + expect(fs.removeSync).not.toHaveBeenCalledWith(debugSigningFile); + expect(fs.removeSync).not.toHaveBeenCalledWith(releaseSigningFile); }); }); }); - describe('apkSorter', () => { + describe('fileSorter', () => { it('should sort APKs from most recent to oldest, deprioritising unsigned arch-specific builds', () => { const APKs = { 'app-debug.apk': new Date('2018-04-20'), @@ -291,40 +287,14 @@ describe('ProjectBuilder', () => { const expectedResult = ['app-release.apk', 'app-debug.apk', 'app-release-unsigned.apk', 'app-release-arm.apk', 'app-release-x86.apk', 'app-debug-x86.apk', 'app-debug-arm.apk']; - const fsSpy = jasmine.createSpyObj('fs', ['statSync']); - fsSpy.statSync.and.callFake(filename => { + spyOn(fs, 'statSync').and.callFake(filename => { return { mtime: APKs[filename] }; }); - ProjectBuilder.__set__('fs', fsSpy); const apkArray = Object.keys(APKs); - const sortedApks = apkArray.sort(ProjectBuilder.__get__('apkSorter')); + const sortedApks = apkArray.sort(ProjectBuilder.__get__('fileSorter')); expect(sortedApks).toEqual(expectedResult); }); }); - - describe('isAutoGenerated', () => { - let fsSpy; - - beforeEach(() => { - fsSpy = jasmine.createSpyObj('fs', ['existsSync', 'readFileSync']); - fsSpy.existsSync.and.returnValue(true); - ProjectBuilder.__set__('fs', fsSpy); - }); - - it('should return true if the file contains the autogenerated marker', () => { - const fileContents = `# DO NOT MODIFY - YOUR CHANGES WILL BE ERASED!`; - fsSpy.readFileSync.and.returnValue(fileContents); - - expect(ProjectBuilder.__get__('isAutoGenerated')()).toBe(true); - }); - - it('should return false if the file does not contain the autogenerated marker', () => { - const fileContents = `# My modified file`; - fsSpy.readFileSync.and.returnValue(fileContents); - - expect(ProjectBuilder.__get__('isAutoGenerated')()).toBe(false); - }); - }); }); diff --git a/spec/unit/check_reqs.spec.js b/spec/unit/check_reqs.spec.js index b70ce757c0..454c0d6dcc 100644 --- a/spec/unit/check_reqs.spec.js +++ b/spec/unit/check_reqs.spec.js @@ -20,10 +20,10 @@ var rewire = require('rewire'); var check_reqs = rewire('../../bin/templates/cordova/lib/check_reqs'); var android_sdk = require('../../bin/templates/cordova/lib/android_sdk'); -var shelljs = require('shelljs'); -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); var events = require('cordova-common').events; +var which = require('which'); // This should match /bin/templates/project/build.gradle const DEFAULT_TARGET_API = 29; @@ -46,7 +46,7 @@ describe('check_reqs', function () { }); describe('even if no Android binaries are on the PATH', function () { beforeEach(function () { - spyOn(shelljs, 'which').and.returnValue(null); + spyOn(which, 'sync').and.returnValue(null); spyOn(fs, 'existsSync').and.returnValue(true); }); it('it should set ANDROID_HOME on Windows', () => { @@ -79,7 +79,7 @@ describe('check_reqs', function () { }); it('should set ANDROID_HOME based on `android` command if command exists in a SDK-like directory structure', () => { spyOn(fs, 'existsSync').and.returnValue(true); - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'android') { return '/android/sdk/tools/android'; } else { @@ -91,7 +91,7 @@ describe('check_reqs', function () { }); }); it('should error out if `android` command exists in a non-SDK-like directory structure', () => { - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'android') { return '/just/some/random/path/android'; } else { @@ -107,7 +107,7 @@ describe('check_reqs', function () { }); it('should set ANDROID_HOME based on `adb` command if command exists in a SDK-like directory structure', () => { spyOn(fs, 'existsSync').and.returnValue(true); - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'adb') { return '/android/sdk/platform-tools/adb'; } else { @@ -119,7 +119,7 @@ describe('check_reqs', function () { }); }); it('should error out if `adb` command exists in a non-SDK-like directory structure', () => { - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'adb') { return '/just/some/random/path/adb'; } else { @@ -135,7 +135,7 @@ describe('check_reqs', function () { }); it('should set ANDROID_HOME based on `avdmanager` command if command exists in a SDK-like directory structure', () => { spyOn(fs, 'existsSync').and.returnValue(true); - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'avdmanager') { return '/android/sdk/tools/bin/avdmanager'; } else { @@ -147,7 +147,7 @@ describe('check_reqs', function () { }); }); it('should error out if `avdmanager` command exists in a non-SDK-like directory structure', () => { - spyOn(shelljs, 'which').and.callFake(function (cmd) { + spyOn(which, 'sync').and.callFake(function (cmd) { if (cmd === 'avdmanager') { return '/just/some/random/path/avdmanager'; } else { @@ -165,7 +165,7 @@ describe('check_reqs', function () { }); describe('set PATH for various Android binaries if not available', function () { beforeEach(function () { - spyOn(shelljs, 'which').and.returnValue(null); + spyOn(which, 'sync').and.returnValue(null); process.env.ANDROID_HOME = 'let the children play'; spyOn(fs, 'existsSync').and.returnValue(true); }); diff --git a/spec/unit/create.spec.js b/spec/unit/create.spec.js index 7eb98fcc3c..ec2c869a96 100644 --- a/spec/unit/create.spec.js +++ b/spec/unit/create.spec.js @@ -18,11 +18,11 @@ */ var rewire = require('rewire'); +var utils = require('../../bin/lib/utils'); var create = rewire('../../bin/lib/create'); var check_reqs = require('../../bin/templates/cordova/lib/check_reqs'); -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); -var shell = require('shelljs'); describe('create', function () { describe('validatePackageName helper method', function () { @@ -53,26 +53,33 @@ describe('create', function () { it('should reject empty package names', () => { return expectPackageNameToBeRejected(''); }); + it('should reject package names containing "class"', () => { return expectPackageNameToBeRejected('com.class.is.bad'); }); + it('should reject package names that do not start with a latin letter', () => { return expectPackageNameToBeRejected('_un.der.score'); }); + it('should reject package names with terms that do not start with a latin letter', () => { return expectPackageNameToBeRejected('un._der.score'); }); + it('should reject package names containing non-alphanumeric or underscore characters', () => { return expectPackageNameToBeRejected('th!$.!$.b@d'); }); + it('should reject package names that do not contain enough dots', () => { return expectPackageNameToBeRejected('therearenodotshere'); }); + it('should reject package names that end with a dot', () => { return expectPackageNameToBeRejected('this.is.a.complete.sentence.'); }); }); }); + describe('validateProjectName helper method', function () { describe('happy path (valid project names)', function () { var valid = [ @@ -90,6 +97,7 @@ describe('create', function () { }); }); }); + describe('failure cases (invalid project names)', function () { it('should reject empty project names', () => { return create.validateProjectName('').then(() => { @@ -101,6 +109,7 @@ describe('create', function () { }); }); }); + describe('main method', function () { var config_mock; var events_mock; @@ -110,6 +119,7 @@ describe('create', function () { var app_path = path.join(project_path, 'app', 'src', 'main'); var default_templates = path.join(__dirname, '..', '..', 'bin', 'templates', 'project'); var fake_android_target = 'android-1337'; + beforeEach(function () { Manifest_mock.prototype = jasmine.createSpyObj('AndroidManifest instance mock', ['setPackageId', 'getActivity', 'setName', 'write']); Manifest_mock.prototype.setPackageId.and.returnValue(new Manifest_mock()); @@ -117,7 +127,6 @@ describe('create', function () { Manifest_mock.prototype.setName.and.returnValue(new Manifest_mock()); spyOn(create, 'validatePackageName').and.resolveTo(); spyOn(create, 'validateProjectName').and.resolveTo(); - spyOn(create, 'setShellFatal').and.callFake(function (noop, cb) { cb(); }); spyOn(create, 'copyJsAndLibrary'); spyOn(create, 'copyScripts'); spyOn(create, 'copyBuildRules'); @@ -125,16 +134,18 @@ describe('create', function () { spyOn(create, 'prepBuildFiles'); revert_manifest_mock = create.__set__('AndroidManifest', Manifest_mock); spyOn(fs, 'existsSync').and.returnValue(false); - spyOn(shell, 'cp'); - spyOn(shell, 'mkdir'); - spyOn(shell, 'sed'); + spyOn(fs, 'copySync'); + spyOn(fs, 'ensureDirSync'); + spyOn(utils, 'replaceFileContents'); config_mock = jasmine.createSpyObj('ConfigParser mock instance', ['packageName', 'android_packageName', 'name', 'android_activityName']); events_mock = jasmine.createSpyObj('EventEmitter mock instance', ['emit']); spyOn(check_reqs, 'get_target').and.returnValue(fake_android_target); }); + afterEach(function () { revert_manifest_mock(); }); + describe('parameter values and defaults', function () { it('should have a default package name of my.cordova.project', () => { config_mock.packageName.and.returnValue(undefined); @@ -142,42 +153,49 @@ describe('create', function () { expect(create.validatePackageName).toHaveBeenCalledWith('my.cordova.project'); }); }); + it('should use the ConfigParser-provided package name, if exists', () => { config_mock.packageName.and.returnValue('org.apache.cordova'); return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.validatePackageName).toHaveBeenCalledWith('org.apache.cordova'); }); }); + it('should have a default project name of CordovaExample', () => { config_mock.name.and.returnValue(undefined); return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.validateProjectName).toHaveBeenCalledWith('CordovaExample'); }); }); + it('should use the ConfigParser-provided project name, if exists', () => { config_mock.name.and.returnValue('MySweetAppName'); return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.validateProjectName).toHaveBeenCalledWith('MySweetAppName'); }); }); + it('should replace any non-word characters (including unicode and spaces) in the ConfigParser-provided project name with underscores', () => { config_mock.name.and.returnValue('応応応応 hello 用用用用'); return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.validateProjectName).toHaveBeenCalledWith('_____hello_____'); }); }); + it('should have a default activity name of MainActivity', () => { config_mock.android_activityName.and.returnValue(undefined); return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(Manifest_mock.prototype.setName).toHaveBeenCalledWith('MainActivity'); }); }); + it('should use the activityName provided via options parameter, if exists', () => { config_mock.android_activityName.and.returnValue(undefined); return create.create(project_path, config_mock, { activityName: 'AwesomeActivity' }, events_mock).then(() => { expect(Manifest_mock.prototype.setName).toHaveBeenCalledWith('AwesomeActivity'); }); }); + it('should use the ConfigParser-provided activity name, if exists', () => { config_mock.android_activityName.and.returnValue('AmazingActivity'); return create.create(project_path, config_mock, {}, events_mock).then(() => { @@ -185,6 +203,7 @@ describe('create', function () { }); }); }); + describe('failure', function () { it('should fail if the target path already exists', () => { fs.existsSync.and.returnValue(true); @@ -195,6 +214,7 @@ describe('create', function () { expect(err.message).toContain('Project already exists!'); }); }); + it('should fail if validateProjectName rejects', () => { const fakeError = new Error(); create.validateProjectName.and.callFake(() => Promise.reject(fakeError)); @@ -207,63 +227,73 @@ describe('create', function () { }); }); + describe('happy path', function () { it('should copy project templates from a specified custom template', () => { return create.create(project_path, config_mock, { customTemplate: '/template/path' }, events_mock).then(() => { - expect(shell.cp).toHaveBeenCalledWith('-r', path.join('/template/path', 'assets'), app_path); - expect(shell.cp).toHaveBeenCalledWith('-r', path.join('/template/path', 'res'), app_path); - expect(shell.cp).toHaveBeenCalledWith(path.join('/template/path', 'gitignore'), path.join(project_path, '.gitignore')); + expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'assets'), path.join(app_path, 'assets')); + expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'res'), path.join(app_path, 'res')); + expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'gitignore'), path.join(project_path, '.gitignore')); }); }); + it('should copy project templates from the default templates location if no custom template is provided', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { - expect(shell.cp).toHaveBeenCalledWith('-r', path.join(default_templates, 'assets'), app_path); - expect(shell.cp).toHaveBeenCalledWith('-r', path.join(default_templates, 'res'), app_path); - expect(shell.cp).toHaveBeenCalledWith(path.join(default_templates, 'gitignore'), path.join(project_path, '.gitignore')); + expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'assets'), path.join(app_path, 'assets')); + expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'res'), path.join(app_path, 'res')); + expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'gitignore'), path.join(project_path, '.gitignore')); }); }); + it('should copy JS and library assets', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.copyJsAndLibrary).toHaveBeenCalled(); }); }); + it('should create a java src directory based on the provided project package name', () => { config_mock.packageName.and.returnValue('org.apache.cordova'); return create.create(project_path, config_mock, {}, events_mock).then(() => { - expect(shell.mkdir).toHaveBeenCalledWith('-p', path.join(app_path, 'java', 'org', 'apache', 'cordova')); + expect(fs.ensureDirSync).toHaveBeenCalledWith(path.join(app_path, 'java', 'org', 'apache', 'cordova')); }); }); + it('should copy, rename and interpolate the template Activity java class with the project-specific activity name and package name', () => { config_mock.packageName.and.returnValue('org.apache.cordova'); config_mock.android_activityName.and.returnValue('CEEDEEVEE'); var activity_path = path.join(app_path, 'java', 'org', 'apache', 'cordova', 'CEEDEEVEE.java'); return create.create(project_path, config_mock, {}, events_mock).then(() => { - expect(shell.cp).toHaveBeenCalledWith('-f', path.join(default_templates, 'Activity.java'), activity_path); - expect(shell.sed).toHaveBeenCalledWith('-i', /__ACTIVITY__/, 'CEEDEEVEE', activity_path); - expect(shell.sed).toHaveBeenCalledWith('-i', /__ID__/, 'org.apache.cordova', activity_path); + expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'Activity.java'), activity_path); + expect(utils.replaceFileContents).toHaveBeenCalledWith(activity_path, /__ACTIVITY__/, 'CEEDEEVEE'); + expect(utils.replaceFileContents).toHaveBeenCalledWith(activity_path, /__ID__/, 'org.apache.cordova'); }); }); + it('should interpolate the project name into strings.xml', () => { config_mock.name.and.returnValue('IncredibleApp'); return create.create(project_path, config_mock, {}, events_mock).then(() => { - expect(shell.sed).toHaveBeenCalledWith('-i', /__NAME__/, 'IncredibleApp', path.join(app_path, 'res', 'values', 'strings.xml')); + expect(utils.replaceFileContents).toHaveBeenCalledWith(path.join(app_path, 'res', 'values', 'strings.xml'), /__NAME__/, 'IncredibleApp'); }); }); + it('should copy template scripts into generated project', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.copyScripts).toHaveBeenCalledWith(project_path); }); }); + it('should copy build rules / gradle files into generated project', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.copyBuildRules).toHaveBeenCalledWith(project_path); }); }); + it('should write project.properties file with project details and target API', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.writeProjectProperties).toHaveBeenCalledWith(project_path, fake_android_target); }); }); + it('should prepare build files', () => { return create.create(project_path, config_mock, {}, events_mock).then(() => { expect(create.prepBuildFiles).toHaveBeenCalledWith(project_path); diff --git a/spec/unit/emulator.spec.js b/spec/unit/emulator.spec.js index 8ae01e5460..8ee203fb54 100644 --- a/spec/unit/emulator.spec.js +++ b/spec/unit/emulator.spec.js @@ -17,10 +17,10 @@ under the License. */ -const fs = require('fs'); +const fs = require('fs-extra'); const path = require('path'); const rewire = require('rewire'); -const shelljs = require('shelljs'); +const which = require('which'); const CordovaError = require('cordova-common').CordovaError; const check_reqs = require('../../bin/templates/cordova/lib/check_reqs'); @@ -83,7 +83,7 @@ describe('emulator', () => { }); it('should try to parse AVD information using `avdmanager` first', () => { - spyOn(shelljs, 'which').and.callFake(cmd => cmd === 'avdmanager'); + spyOn(which, 'sync').and.callFake(cmd => cmd === 'avdmanager'); const avdmanager_spy = spyOn(emu, 'list_images_using_avdmanager').and.returnValue(Promise.resolve([])); @@ -93,7 +93,7 @@ describe('emulator', () => { }); it('should delegate to `android` if `avdmanager` cant be found and `android` can', () => { - spyOn(shelljs, 'which').and.callFake(cmd => cmd !== 'avdmanager'); + spyOn(which, 'sync').and.callFake(cmd => cmd !== 'avdmanager'); const android_spy = spyOn(emu, 'list_images_using_android').and.returnValue(Promise.resolve([])); @@ -103,7 +103,7 @@ describe('emulator', () => { }); it('should correct api level information and fill in the blanks about api level if exists', () => { - spyOn(shelljs, 'which').and.callFake(cmd => cmd === 'avdmanager'); + spyOn(which, 'sync').and.callFake(cmd => cmd === 'avdmanager'); spyOn(emu, 'list_images_using_avdmanager').and.returnValue(Promise.resolve([ { name: 'Pixel_7.0', @@ -127,7 +127,7 @@ describe('emulator', () => { }); it('should throw an error if neither `avdmanager` nor `android` are able to be found', () => { - spyOn(shelljs, 'which').and.returnValue(false); + spyOn(which, 'sync').and.returnValue(false); return emu.list_images().then( () => fail('Unexpectedly resolved'), @@ -255,7 +255,7 @@ describe('emulator', () => { let AdbSpy; let checkReqsSpy; let execaSpy; - let shellJsSpy; + let whichSpy; beforeEach(() => { emulator = { @@ -286,9 +286,9 @@ describe('emulator', () => { const proc = emu.__get__('process'); spyOn(proc.stdout, 'write').and.stub(); - shellJsSpy = jasmine.createSpyObj('shelljs', ['which']); - shellJsSpy.which.and.returnValue('/dev/android-sdk/tools'); - emu.__set__('shelljs', shellJsSpy); + whichSpy = jasmine.createSpyObj('which', ['sync']); + whichSpy.sync.and.returnValue('/dev/android-sdk/tools'); + emu.__set__('which', whichSpy); }); it('should find an emulator if an id is not specified', () => { diff --git a/spec/unit/pluginHandlers/common.spec.js b/spec/unit/pluginHandlers/common.spec.js index e0161febb9..ead1bb9826 100644 --- a/spec/unit/pluginHandlers/common.spec.js +++ b/spec/unit/pluginHandlers/common.spec.js @@ -19,9 +19,8 @@ var rewire = require('rewire'); var common = rewire('../../../bin/templates/cordova/lib/pluginHandlers'); var path = require('path'); -var fs = require('fs'); +var fs = require('fs-extra'); var osenv = require('os'); -var shell = require('shelljs'); var test_dir = path.join(osenv.tmpdir(), 'test_plugman'); var project_dir = path.join(test_dir, 'project'); var src = path.join(project_dir, 'src'); @@ -39,22 +38,22 @@ describe('common platform handler', function () { describe('copyFile', function () { it('Test#001 : should throw if source path not found', function () { - shell.rm('-rf', src); + fs.removeSync(src); expect(function () { copyFile(test_dir, src, project_dir, dest); }) .toThrow(new Error('"' + src + '" not found!')); }); it('Test#002 : should throw if src not in plugin directory', function () { - shell.mkdir('-p', project_dir); + fs.ensureDirSync(project_dir); fs.writeFileSync(non_plugin_file, 'contents', 'utf-8'); var outside_file = '../non_plugin_file'; expect(function () { copyFile(test_dir, outside_file, project_dir, dest); }) .toThrow(new Error('File "' + path.resolve(test_dir, outside_file) + '" is located outside the plugin directory "' + test_dir + '"')); - shell.rm('-rf', test_dir); + fs.removeSync(test_dir); }); it('Test#003 : should allow symlink src, if inside plugin', function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(java_file, 'contents', 'utf-8'); // This will fail on windows if not admin - ignore the error in that case. @@ -63,11 +62,11 @@ describe('common platform handler', function () { } copyFile(test_dir, symlink_file, project_dir, dest); - shell.rm('-rf', project_dir); + fs.removeSync(project_dir); }); it('Test#004 : should throw if symlink is linked to a file outside the plugin', function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(non_plugin_file, 'contents', 'utf-8'); // This will fail on windows if not admin - ignore the error in that case. @@ -77,68 +76,68 @@ describe('common platform handler', function () { expect(function () { copyFile(test_dir, symlink_file, project_dir, dest); }) .toThrow(new Error('File "' + path.resolve(test_dir, symlink_file) + '" is located outside the plugin directory "' + test_dir + '"')); - shell.rm('-rf', project_dir); + fs.removeSync(project_dir); }); it('Test#005 : should throw if dest is outside the project directory', function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(java_file, 'contents', 'utf-8'); expect(function () { copyFile(test_dir, java_file, project_dir, non_plugin_file); }) .toThrow(new Error('Destination "' + path.resolve(project_dir, non_plugin_file) + '" for source file "' + path.resolve(test_dir, java_file) + '" is located outside the project')); - shell.rm('-rf', project_dir); + fs.removeSync(project_dir); }); it('Test#006 : should call mkdir -p on target path', function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(java_file, 'contents', 'utf-8'); - var s = spyOn(shell, 'mkdir').and.callThrough(); + var s = spyOn(fs, 'ensureDirSync').and.callThrough(); var resolvedDest = path.resolve(project_dir, dest); copyFile(test_dir, java_file, project_dir, dest); expect(s).toHaveBeenCalled(); - expect(s).toHaveBeenCalledWith('-p', path.dirname(resolvedDest)); - shell.rm('-rf', project_dir); + expect(s).toHaveBeenCalledWith(path.dirname(resolvedDest)); + fs.removeSync(project_dir); }); it('Test#007 : should call cp source/dest paths', function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(java_file, 'contents', 'utf-8'); - var s = spyOn(shell, 'cp').and.callThrough(); + var s = spyOn(fs, 'copySync').and.callThrough(); var resolvedDest = path.resolve(project_dir, dest); copyFile(test_dir, java_file, project_dir, dest); expect(s).toHaveBeenCalled(); - expect(s).toHaveBeenCalledWith('-f', java_file, resolvedDest); + expect(s).toHaveBeenCalledWith(java_file, resolvedDest); - shell.rm('-rf', project_dir); + fs.removeSync(project_dir); }); }); describe('copyNewFile', function () { it('Test#008 : should throw if target path exists', function () { - shell.mkdir('-p', dest); + fs.ensureDirSync(dest); expect(function () { copyNewFile(test_dir, src, project_dir, dest); }) .toThrow(new Error('"' + dest + '" already exists!')); - shell.rm('-rf', dest); + fs.removeSync(dest); }); }); describe('deleteJava', function () { beforeEach(function () { - shell.mkdir('-p', java_dir); + fs.ensureDirSync(java_dir); fs.writeFileSync(java_file, 'contents', 'utf-8'); }); afterEach(function () { - shell.rm('-rf', java_dir); + fs.removeSync(java_dir); }); it('Test#009 : should call fs.unlinkSync on the provided paths', function () { - var s = spyOn(fs, 'unlinkSync').and.callThrough(); + var s = spyOn(fs, 'removeSync').and.callThrough(); deleteJava(project_dir, java_file); expect(s).toHaveBeenCalled(); expect(s).toHaveBeenCalledWith(path.resolve(project_dir, java_file)); diff --git a/spec/unit/pluginHandlers/handlers.spec.js b/spec/unit/pluginHandlers/handlers.spec.js index b712214831..ee41424f85 100644 --- a/spec/unit/pluginHandlers/handlers.spec.js +++ b/spec/unit/pluginHandlers/handlers.spec.js @@ -21,14 +21,13 @@ var rewire = require('rewire'); var common = rewire('../../../bin/templates/cordova/lib/pluginHandlers'); var android = common.__get__('handlers'); var path = require('path'); -var fs = require('fs'); -var shell = require('shelljs'); +var fs = require('fs-extra'); var os = require('os'); var temp = path.join(os.tmpdir(), 'plugman'); var plugins_dir = path.join(temp, 'cordova/plugins'); var dummyplugin = path.join(__dirname, '../../fixtures/org.test.plugins.dummyplugin'); var faultyplugin = path.join(__dirname, '../../fixtures/org.test.plugins.faultyplugin'); -var android_studio_project = path.join(__dirname, '../../fixtures/android_studio_project/*'); +var android_studio_project = path.join(__dirname, '../../fixtures/android_studio_project'); var PluginInfo = require('cordova-common').PluginInfo; var AndroidProject = require('../../../bin/templates/cordova/lib/AndroidProject'); @@ -48,14 +47,14 @@ describe('android project handler', function () { var dummyProject; beforeEach(function () { - shell.mkdir('-p', temp); + fs.ensureDirSync(temp); dummyProject = AndroidProject.getProjectFile(temp); copyFileSpy.calls.reset(); common.__set__('copyFile', copyFileSpy); }); afterEach(function () { - shell.rm('-rf', temp); + fs.removeSync(temp); common.__set__('copyFile', copyFileOrig); }); @@ -75,7 +74,7 @@ describe('android project handler', function () { describe('of elements', function () { beforeEach(function () { - shell.cp('-rf', android_studio_project, temp); + fs.copySync(android_studio_project, temp); }); it('Test#003 : should copy stuff from one location to another by calling common.copyFile', function () { @@ -94,7 +93,7 @@ describe('android project handler', function () { it('Test#006 : should throw if target file already exists', function () { // write out a file let target = path.resolve(temp, 'app', 'src', 'main', 'java', 'com', 'phonegap', 'plugins', 'dummyplugin'); - shell.mkdir('-p', target); + fs.ensureDirSync(target); target = path.join(target, 'DummyPlugin.java'); fs.writeFileSync(target, 'some bs', 'utf-8'); @@ -185,7 +184,7 @@ describe('android project handler', function () { var copyNewFileSpy = jasmine.createSpy('copyNewFile'); beforeEach(function () { - shell.cp('-rf', android_studio_project, temp); + fs.copySync(android_studio_project, temp); spyOn(dummyProject, 'addSystemLibrary'); spyOn(dummyProject, 'addSubProject'); @@ -215,7 +214,7 @@ describe('android project handler', function () { it('Test#010 : should not copy anything if "custom" attribute is not set', function () { var framework = { src: 'plugin-lib' }; - var cpSpy = spyOn(shell, 'cp'); + var cpSpy = spyOn(fs, 'copySync'); android.framework.install(framework, dummyPluginInfo, dummyProject); expect(dummyProject.addSystemLibrary).toHaveBeenCalledWith(someString, framework.src); expect(cpSpy).not.toHaveBeenCalled(); @@ -282,26 +281,24 @@ describe('android project handler', function () { describe('uninstallation', function () { - var removeFileOrig = common.__get__('removeFile'); var deleteJavaOrig = common.__get__('deleteJava'); - - var removeFileSpy = jasmine.createSpy('removeFile'); + let originalRemoveSync = fs.removeSync; var deleteJavaSpy = jasmine.createSpy('deleteJava'); var dummyProject; + let removeSyncSpy; beforeEach(function () { - shell.mkdir('-p', temp); - shell.mkdir('-p', plugins_dir); - shell.cp('-rf', android_studio_project, temp); + fs.ensureDirSync(temp); + fs.ensureDirSync(plugins_dir); + fs.copySync(android_studio_project, temp); AndroidProject.purgeCache(); dummyProject = AndroidProject.getProjectFile(temp); - common.__set__('removeFile', removeFileSpy); + removeSyncSpy = spyOn(fs, 'removeSync'); common.__set__('deleteJava', deleteJavaSpy); }); afterEach(function () { - shell.rm('-rf', temp); - common.__set__('removeFile', removeFileOrig); + originalRemoveSync.call(fs, temp); common.__set__('deleteJava', deleteJavaOrig); }); @@ -309,7 +306,7 @@ describe('android project handler', function () { it('Test#017 : should remove jar files for Android Studio projects', function () { android['lib-file'].install(valid_libs[0], dummyPluginInfo, dummyProject); android['lib-file'].uninstall(valid_libs[0], dummyPluginInfo, dummyProject); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/libs/TestLib.jar')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/libs/TestLib.jar')); }); }); @@ -317,7 +314,7 @@ describe('android project handler', function () { it('Test#018 : should remove files for Android Studio projects', function () { android['resource-file'].install(valid_resources[0], dummyPluginInfo, dummyProject); android['resource-file'].uninstall(valid_resources[0], dummyPluginInfo, dummyProject); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app', 'src', 'main', 'res', 'xml', 'dummy.xml')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app', 'src', 'main', 'res', 'xml', 'dummy.xml')); }); }); @@ -337,55 +334,55 @@ describe('android project handler', function () { it('Test#019b : should remove stuff by calling common.removeFile for Android Studio projects, of jar with new app target-dir scheme', function () { android['source-file'].install(valid_source[2], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[2], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/libs/TestLib.jar')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/libs/TestLib.jar')); }); it('Test#019c : should remove stuff by calling common.removeFile for Android Studio projects, of aar with new app target-dir scheme', function () { android['source-file'].install(valid_source[3], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[3], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/libs/TestAar.aar')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/libs/TestAar.aar')); }); it('Test#019d : should remove stuff by calling common.removeFile for Android Studio projects, of xml with old target-dir scheme', function () { android['source-file'].install(valid_source[4], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[4], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/src/main/res/xml/mysettings.xml')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/src/main/res/xml/mysettings.xml')); }); it('Test#019e : should remove stuff by calling common.removeFile for Android Studio projects, of file with other extension with old target-dir scheme', function () { android['source-file'].install(valid_source[5], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[5], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/src/main/res/values/other.extension')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/src/main/res/values/other.extension')); }); it('Test#019f : should remove stuff by calling common.removeFile for Android Studio projects, of aidl with old target-dir scheme (GH-547)', function () { android['source-file'].install(valid_source[6], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[6], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/src/main/aidl/com/mytest/myapi.aidl')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/src/main/aidl/com/mytest/myapi.aidl')); }); it('Test#019g : should remove stuff by calling common.removeFile for Android Studio projects, of aar with old target-dir scheme (GH-547)', function () { android['source-file'].install(valid_source[7], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[7], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/libs/testaar2.aar')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/libs/testaar2.aar')); }); it('Test#019h : should remove stuff by calling common.removeFile for Android Studio projects, of jar with old target-dir scheme (GH-547)', function () { android['source-file'].install(valid_source[8], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[8], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/libs/testjar2.jar')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/libs/testjar2.jar')); }); it('Test#019i : should remove stuff by calling common.removeFile for Android Studio projects, of .so lib file with old target-dir scheme (GH-547)', function () { android['source-file'].install(valid_source[9], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[9], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(removeFileSpy).toHaveBeenCalledWith(temp, path.join('app/src/main/jniLibs/x86/libnative.so')); + expect(removeSyncSpy).toHaveBeenCalledWith(path.join(dummyProject.projectDir, 'app/src/main/jniLibs/x86/libnative.so')); }); it('Test#019j : should remove stuff by calling common.deleteJava for Android Studio projects, with target-dir that includes "app"', function () { android['source-file'].install(valid_source[10], dummyPluginInfo, dummyProject, { android_studio: true }); android['source-file'].uninstall(valid_source[10], dummyPluginInfo, dummyProject, { android_studio: true }); - expect(deleteJavaSpy).toHaveBeenCalledWith(temp, path.join('app/src/main/java/com/appco/DummyPlugin2.java')); + expect(deleteJavaSpy).toHaveBeenCalledWith(dummyProject.projectDir, path.join('app/src/main/java/com/appco/DummyPlugin2.java')); }); }); @@ -394,7 +391,7 @@ describe('android project handler', function () { var someString = jasmine.any(String); beforeEach(function () { - shell.mkdir(path.join(dummyProject.projectDir, dummyPluginInfo.id)); + fs.ensureDirSync(path.join(dummyProject.projectDir, dummyPluginInfo.id)); spyOn(dummyProject, 'removeSystemLibrary'); spyOn(dummyProject, 'removeSubProject'); @@ -421,13 +418,13 @@ describe('android project handler', function () { var framework = { src: 'plugin-lib', custom: true }; android.framework.uninstall(framework, dummyPluginInfo, dummyProject); expect(dummyProject.removeSubProject).toHaveBeenCalledWith(dummyProject.projectDir, someString); - expect(removeFileSpy).toHaveBeenCalledWith(dummyProject.projectDir, someString); + expect(removeSyncSpy).toHaveBeenCalledWith(someString); }); it('Test#24 : should install gradleReference using project.removeGradleReference', function () { var framework = { src: 'plugin-lib', custom: true, type: 'gradleReference' }; android.framework.uninstall(framework, dummyPluginInfo, dummyProject); - expect(removeFileSpy).toHaveBeenCalledWith(dummyProject.projectDir, someString); + expect(removeSyncSpy).toHaveBeenCalledWith(someString); expect(dummyProject.removeGradleReference).toHaveBeenCalledWith(dummyProject.projectDir, someString); }); }); @@ -441,8 +438,6 @@ describe('android project handler', function () { wwwDest = path.resolve(dummyProject.www, 'plugins', dummyPluginInfo.id, jsModule.src); platformWwwDest = path.resolve(dummyProject.platformWww, 'plugins', dummyPluginInfo.id, jsModule.src); - spyOn(shell, 'rm'); - var existsSyncOrig = fs.existsSync; spyOn(fs, 'existsSync').and.callFake(function (file) { if ([wwwDest, platformWwwDest].indexOf(file) >= 0) return true; @@ -452,14 +447,14 @@ describe('android project handler', function () { it('Test#025 : should put module to both www and platform_www when options.usePlatformWww flag is specified', function () { android['js-module'].uninstall(jsModule, dummyPluginInfo, dummyProject, { usePlatformWww: true }); - expect(shell.rm).toHaveBeenCalledWith('-Rf', wwwDest); - expect(shell.rm).toHaveBeenCalledWith('-Rf', platformWwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(wwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(platformWwwDest); }); it('Test#026 : should put module to www only when options.usePlatformWww flag is not specified', function () { android['js-module'].uninstall(jsModule, dummyPluginInfo, dummyProject); - expect(shell.rm).toHaveBeenCalledWith('-Rf', wwwDest); - expect(shell.rm).not.toHaveBeenCalledWith('-Rf', platformWwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(wwwDest); + expect(removeSyncSpy).not.toHaveBeenCalledWith(platformWwwDest); }); }); @@ -471,8 +466,6 @@ describe('android project handler', function () { wwwDest = path.resolve(dummyProject.www, asset.target); platformWwwDest = path.resolve(dummyProject.platformWww, asset.target); - spyOn(shell, 'rm'); - var existsSyncOrig = fs.existsSync; spyOn(fs, 'existsSync').and.callFake(function (file) { if ([wwwDest, platformWwwDest].indexOf(file) >= 0) return true; @@ -482,14 +475,14 @@ describe('android project handler', function () { it('Test#027 : should put module to both www and platform_www when options.usePlatformWww flag is specified', function () { android.asset.uninstall(asset, dummyPluginInfo, dummyProject, { usePlatformWww: true }); - expect(shell.rm).toHaveBeenCalledWith(jasmine.any(String), wwwDest); - expect(shell.rm).toHaveBeenCalledWith(jasmine.any(String), platformWwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(wwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(platformWwwDest); }); it('Test#028 : should put module to www only when options.usePlatformWww flag is not specified', function () { android.asset.uninstall(asset, dummyPluginInfo, dummyProject); - expect(shell.rm).toHaveBeenCalledWith(jasmine.any(String), wwwDest); - expect(shell.rm).not.toHaveBeenCalledWith(jasmine.any(String), platformWwwDest); + expect(removeSyncSpy).toHaveBeenCalledWith(wwwDest); + expect(removeSyncSpy).not.toHaveBeenCalledWith(platformWwwDest); }); }); });