UNPKG

@appsonair/codepush-cli

Version:
1,121 lines (1,120 loc) 56.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.runReactNativeBundleCommand = exports.releaseReact = exports.release = exports.execute = exports.deploymentList = exports.createEmptyTempReleaseFolder = exports.confirm = exports.execSync = exports.spawn = exports.sdk = exports.log = void 0; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. const AccountManager = require("./management-sdk"); const childProcess = require("child_process"); const debug_1 = require("./commands/debug"); const fs = require("fs"); const chalk = require("chalk"); const g2js = require("gradle-to-js/lib/parser"); const moment = require("moment"); const opener = require("opener"); const os = require("os"); const path = require("path"); const plist = require("plist"); const progress = require("progress"); const prompt = require("prompt"); const Q = require("q"); const rimraf = require("rimraf"); const semver = require("semver"); const Table = require("cli-table"); const which = require("which"); const wordwrap = require("wordwrap"); const cli = require("../script/types/cli"); const sign_1 = require("./sign"); const xcode = require("xcode"); const react_native_utils_1 = require("./react-native-utils"); const file_utils_1 = require("./utils/file-utils"); const configFilePath = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); const emailValidator = require("email-validator"); const packageJson = require("../../package.json"); const parseXml = Q.denodeify(require("xml2js").parseString); var Promise = Q.Promise; const properties = require("properties"); const CLI_HEADERS = { "X-CodePush-CLI-Version": packageJson.version, }; const log = (message) => console.log(message); exports.log = log; exports.spawn = childProcess.spawn; exports.execSync = childProcess.execSync; let connectionInfo; const confirm = (message = "Are you sure?") => { message += " (y/N):"; return Promise((resolve, reject, notify) => { prompt.message = ""; prompt.delimiter = ""; prompt.start(); prompt.get({ properties: { response: { description: chalk.cyan(message), }, }, }, (err, result) => { const accepted = result.response && result.response.toLowerCase() === "y"; const rejected = !result.response || result.response.toLowerCase() === "n"; if (accepted) { resolve(true); } else { if (!rejected) { console.log('Invalid response: "' + result.response + '"'); } resolve(false); } }); }); }; exports.confirm = confirm; function accessKeyAdd(command) { return exports.sdk.addAccessKey(command.name, command.ttl).then((accessKey) => { (0, exports.log)(`Successfully created the "${command.name}" access key: ${accessKey.key}`); (0, exports.log)("Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!"); }); } function accessKeyPatch(command) { const willUpdateName = isCommandOptionSpecified(command.newName) && command.oldName !== command.newName; const willUpdateTtl = isCommandOptionSpecified(command.ttl); if (!willUpdateName && !willUpdateTtl) { throw new Error("A new name and/or TTL must be provided."); } return exports.sdk.patchAccessKey(command.oldName, command.newName, command.ttl).then((accessKey) => { let logMessage = "Successfully "; if (willUpdateName) { logMessage += `renamed the access key "${command.oldName}" to "${command.newName}"`; } if (willUpdateTtl) { const expirationDate = moment(accessKey.expires).format("LLLL"); if (willUpdateName) { logMessage += ` and changed its expiration date to ${expirationDate}`; } else { logMessage += `changed the expiration date of the "${command.oldName}" access key to ${expirationDate}`; } } (0, exports.log)(`${logMessage}.`); }); } function accessKeyList(command) { throwForInvalidOutputFormat(command.format); return exports.sdk.getAccessKeys().then((accessKeys) => { printAccessKeys(command.format, accessKeys); }); } function accessKeyRemove(command) { return (0, exports.confirm)().then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.removeAccessKey(command.accessKey).then(() => { (0, exports.log)(`Successfully removed the "${command.accessKey}" access key.`); }); } (0, exports.log)("Access key removal cancelled."); }); } function appAdd(command) { return exports.sdk.addApp(command.appName).then((app) => { (0, exports.log)('Successfully added the "' + command.appName + '" app, along with the following default deployments:'); const deploymentListCommand = { type: cli.CommandType.deploymentList, appName: app.name, format: "table", displayKeys: true, }; return (0, exports.deploymentList)(deploymentListCommand, /*showPackage=*/ false); }); } function appList(command) { throwForInvalidOutputFormat(command.format); let apps; return exports.sdk.getApps().then((retrievedApps) => { printAppList(command.format, retrievedApps); }); } function appRemove(command) { return (0, exports.confirm)("Are you sure you want to remove this app? Note that its deployment keys will be PERMANENTLY unrecoverable.").then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.removeApp(command.appName).then(() => { (0, exports.log)('Successfully removed the "' + command.appName + '" app.'); }); } (0, exports.log)("App removal cancelled."); }); } function appRename(command) { return exports.sdk.renameApp(command.currentAppName, command.newAppName).then(() => { (0, exports.log)('Successfully renamed the "' + command.currentAppName + '" app to "' + command.newAppName + '".'); }); } const createEmptyTempReleaseFolder = (folderPath) => { return deleteFolder(folderPath).then(() => { fs.mkdirSync(folderPath); }); }; exports.createEmptyTempReleaseFolder = createEmptyTempReleaseFolder; function appTransfer(command) { throwForInvalidEmail(command.email); return (0, exports.confirm)().then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.transferApp(command.appName, command.email).then(() => { (0, exports.log)('Successfully transferred the ownership of app "' + command.appName + '" to the account with email "' + command.email + '".'); }); } (0, exports.log)("App transfer cancelled."); }); } function addCollaborator(command) { throwForInvalidEmail(command.email); return exports.sdk.addCollaborator(command.appName, command.email).then(() => { (0, exports.log)('Successfully added "' + command.email + '" as a collaborator to the app "' + command.appName + '".'); }); } function listCollaborators(command) { throwForInvalidOutputFormat(command.format); return exports.sdk.getCollaborators(command.appName).then((retrievedCollaborators) => { printCollaboratorsList(command.format, retrievedCollaborators); }); } function removeCollaborator(command) { throwForInvalidEmail(command.email); return (0, exports.confirm)().then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.removeCollaborator(command.appName, command.email).then(() => { (0, exports.log)('Successfully removed "' + command.email + '" as a collaborator from the app "' + command.appName + '".'); }); } (0, exports.log)("App collaborator removal cancelled."); }); } function deleteConnectionInfoCache(printMessage = true) { try { fs.unlinkSync(configFilePath); if (printMessage) { (0, exports.log)(`Successfully logged-out. The session file located at ${chalk.cyan(configFilePath)} has been deleted.\r\n`); } } catch (ex) { } } function deleteFolder(folderPath) { return Promise((resolve, reject, notify) => { rimraf(folderPath, (err) => { if (err) { reject(err); } else { resolve(null); } }); }); } function deploymentAdd(command) { return exports.sdk.addDeployment(command.appName, command.deploymentName, command.key).then((deployment) => { (0, exports.log)('Successfully added the "' + command.deploymentName + '" deployment with key "' + deployment.key + '" to the "' + command.appName + '" app.'); }); } function deploymentHistoryClear(command) { return (0, exports.confirm)().then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.clearDeploymentHistory(command.appName, command.deploymentName).then(() => { (0, exports.log)('Successfully cleared the release history associated with the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.'); }); } (0, exports.log)("Clear deployment cancelled."); }); } const deploymentList = (command, showPackage = true) => { throwForInvalidOutputFormat(command.format); let deployments; return exports.sdk .getDeployments(command.appName) .then((retrievedDeployments) => { deployments = retrievedDeployments; if (showPackage) { const metricsPromises = deployments.map((deployment) => { if (deployment.package) { return exports.sdk.getDeploymentMetrics(command.appName, deployment.name).then((metrics) => { if (metrics[deployment.package.label]) { const totalActive = getTotalActiveFromDeploymentMetrics(metrics); deployment.package.metrics = { active: metrics[deployment.package.label].active, downloaded: metrics[deployment.package.label].downloaded, failed: metrics[deployment.package.label].failed, installed: metrics[deployment.package.label].installed, totalActive: totalActive, }; } }); } else { return Q(null); } }); return Q.all(metricsPromises); } }) .then(() => { printDeploymentList(command, deployments, showPackage); }); }; exports.deploymentList = deploymentList; function deploymentRemove(command) { return (0, exports.confirm)("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.").then((wasConfirmed) => { if (wasConfirmed) { return exports.sdk.removeDeployment(command.appName, command.deploymentName).then(() => { (0, exports.log)('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.'); }); } (0, exports.log)("Deployment removal cancelled."); }); } function deploymentRename(command) { return exports.sdk.renameDeployment(command.appName, command.currentDeploymentName, command.newDeploymentName).then(() => { (0, exports.log)('Successfully renamed the "' + command.currentDeploymentName + '" deployment to "' + command.newDeploymentName + '" for the "' + command.appName + '" app.'); }); } function deploymentHistory(command) { throwForInvalidOutputFormat(command.format); return Q.all([ exports.sdk.getAccountInfo(), exports.sdk.getDeploymentHistory(command.appName, command.deploymentName), exports.sdk.getDeploymentMetrics(command.appName, command.deploymentName), ]).spread((account, deploymentHistory, metrics) => { const totalActive = getTotalActiveFromDeploymentMetrics(metrics); deploymentHistory.forEach((packageObject) => { if (metrics[packageObject.label]) { packageObject.metrics = { active: metrics[packageObject.label].active, downloaded: metrics[packageObject.label].downloaded, failed: metrics[packageObject.label].failed, installed: metrics[packageObject.label].installed, totalActive: totalActive, }; } }); printDeploymentHistory(command, deploymentHistory, account.email); }); } function deserializeConnectionInfo() { try { const savedConnection = fs.readFileSync(configFilePath, { encoding: "utf8", }); let connectionInfo = JSON.parse(savedConnection); // If the connection info is in the legacy format, convert it to the modern format if (connectionInfo.accessKeyName) { connectionInfo = { accessKey: connectionInfo.accessKeyName, }; } const connInfo = connectionInfo; return connInfo; } catch (ex) { return; } } function execute(command) { connectionInfo = deserializeConnectionInfo(); return Q(null).then(() => { switch (command.type) { // Must not be logged in case cli.CommandType.login: case cli.CommandType.register: if (connectionInfo) { throw new Error("You are already logged in from this machine."); } break; // It does not matter whether you are logged in or not case cli.CommandType.link: break; // Must be logged in default: if (!!exports.sdk) break; // Used by unit tests to skip authentication if (!connectionInfo) { throw new Error("You are not currently logged in. Run the 'appsonair-codepush login' command to authenticate with the CodePush server."); } exports.sdk = getSdk(connectionInfo.accessKey, CLI_HEADERS, connectionInfo.customServerUrl); break; } switch (command.type) { case cli.CommandType.accessKeyAdd: return accessKeyAdd(command); case cli.CommandType.accessKeyPatch: return accessKeyPatch(command); case cli.CommandType.accessKeyList: return accessKeyList(command); case cli.CommandType.accessKeyRemove: return accessKeyRemove(command); case cli.CommandType.appAdd: return appAdd(command); case cli.CommandType.appList: return appList(command); case cli.CommandType.appRemove: return appRemove(command); case cli.CommandType.appRename: return appRename(command); case cli.CommandType.appTransfer: return appTransfer(command); case cli.CommandType.collaboratorAdd: return addCollaborator(command); case cli.CommandType.collaboratorList: return listCollaborators(command); case cli.CommandType.collaboratorRemove: return removeCollaborator(command); case cli.CommandType.debug: return (0, debug_1.default)(command); case cli.CommandType.deploymentAdd: return deploymentAdd(command); case cli.CommandType.deploymentHistoryClear: return deploymentHistoryClear(command); case cli.CommandType.deploymentHistory: return deploymentHistory(command); case cli.CommandType.deploymentList: return (0, exports.deploymentList)(command); case cli.CommandType.deploymentRemove: return deploymentRemove(command); case cli.CommandType.deploymentRename: return deploymentRename(command); case cli.CommandType.link: return link(command); case cli.CommandType.login: return login(command); case cli.CommandType.logout: return logout(command); case cli.CommandType.patch: return patch(command); case cli.CommandType.promote: return promote(command); case cli.CommandType.register: return register(command); case cli.CommandType.release: return (0, exports.release)(command); case cli.CommandType.releaseReact: return (0, exports.releaseReact)(command); case cli.CommandType.rollback: return rollback(command); case cli.CommandType.sessionList: return sessionList(command); case cli.CommandType.sessionRemove: return sessionRemove(command); case cli.CommandType.whoami: return whoami(command); default: // We should never see this message as invalid commands should be caught by the argument parser. throw new Error("Invalid command: " + JSON.stringify(command)); } }); } exports.execute = execute; function getTotalActiveFromDeploymentMetrics(metrics) { let totalActive = 0; Object.keys(metrics).forEach((label) => { totalActive += metrics[label].active; }); return totalActive; } function initiateExternalAuthenticationAsync(action, serverUrl) { const message = `A browser is being launched to authenticate your account. Follow the instructions ` + `it displays to complete your ${action === "register" ? "registration" : action}.`; (0, exports.log)(message); const hostname = os.hostname(); const url = `${serverUrl || AccountManager.SERVER_URL}/auth/${action}?hostname=${hostname}`; opener(url); } function link(command) { initiateExternalAuthenticationAsync("link", command.serverUrl); return Q(null); } function login(command) { // Check if one of the flags were provided. if (command.accessKey) { exports.sdk = getSdk(command.accessKey, CLI_HEADERS, command.serverUrl); return exports.sdk.isAuthenticated().then((isAuthenticated) => { if (isAuthenticated) { serializeConnectionInfo(command.accessKey, /*preserveAccessKeyOnLogout*/ true, command.serverUrl); } else { throw new Error("Invalid access key."); } }); } else { return loginWithExternalAuthentication("login", command.serverUrl); } } function loginWithExternalAuthentication(action, serverUrl) { initiateExternalAuthenticationAsync(action, serverUrl); (0, exports.log)(""); // Insert newline return requestAccessKey().then((accessKey) => { if (accessKey === null) { // The user has aborted the synchronous prompt (e.g.: via [CTRL]+[C]). return; } exports.sdk = getSdk(accessKey, CLI_HEADERS, serverUrl); return exports.sdk.isAuthenticated().then((isAuthenticated) => { if (isAuthenticated) { serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl); } else { throw new Error("Invalid access key."); } }); }); } function logout(command) { return Q(null) .then(() => { if (!connectionInfo.preserveAccessKeyOnLogout) { const machineName = os.hostname(); return exports.sdk.removeSession(machineName).catch((error) => { // If we are not authenticated or the session doesn't exist anymore, just swallow the error instead of displaying it if (error.statusCode !== AccountManager.ERROR_UNAUTHORIZED && error.statusCode !== AccountManager.ERROR_NOT_FOUND) { throw error; } }); } }) .then(() => { exports.sdk = null; deleteConnectionInfoCache(); }); } function formatDate(unixOffset) { const date = moment(unixOffset); const now = moment(); if (Math.abs(now.diff(date, "days")) < 30) { return date.fromNow(); // "2 hours ago" } else if (now.year() === date.year()) { return date.format("MMM D"); // "Nov 6" } else { return date.format("MMM D, YYYY"); // "Nov 6, 2014" } } function printAppList(format, apps) { if (format === "json") { printJson(apps); } else if (format === "table") { const headers = ["Name", "Deployments"]; printTable(headers, (dataSource) => { apps.forEach((app, index) => { const row = [app.name, wordwrap(50)(app.deployments.join(", "))]; dataSource.push(row); }); }); } } function getCollaboratorDisplayName(email, collaboratorProperties) { return collaboratorProperties.permission === AccountManager.AppPermission.OWNER ? email + chalk.magenta(" (Owner)") : email; } function printCollaboratorsList(format, collaborators) { if (format === "json") { const dataSource = { collaborators: collaborators }; printJson(dataSource); } else if (format === "table") { const headers = ["E-mail Address"]; printTable(headers, (dataSource) => { Object.keys(collaborators).forEach((email) => { const row = [getCollaboratorDisplayName(email, collaborators[email])]; dataSource.push(row); }); }); } } function printDeploymentList(command, deployments, showPackage = true) { if (command.format === "json") { printJson(deployments); } else if (command.format === "table") { const headers = ["Name"]; if (command.displayKeys) { headers.push("Deployment Key"); } if (showPackage) { headers.push("Update Metadata"); headers.push("Install Metrics"); } printTable(headers, (dataSource) => { deployments.forEach((deployment) => { const row = [deployment.name]; if (command.displayKeys) { row.push(deployment.key); } if (showPackage) { row.push(getPackageString(deployment.package)); row.push(getPackageMetricsString(deployment.package)); } dataSource.push(row); }); }); } } function printDeploymentHistory(command, deploymentHistory, currentUserEmail) { if (command.format === "json") { printJson(deploymentHistory); } else if (command.format === "table") { const headers = ["Label", "Release Time", "App Version", "Mandatory"]; if (command.displayAuthor) { headers.push("Released By"); } headers.push("Description", "Install Metrics"); printTable(headers, (dataSource) => { deploymentHistory.forEach((packageObject) => { let releaseTime = formatDate(packageObject.uploadTime); let releaseSource; if (packageObject.releaseMethod === "Promote") { releaseSource = `Promoted ${packageObject.originalLabel} from "${packageObject.originalDeployment}"`; } else if (packageObject.releaseMethod === "Rollback") { const labelNumber = parseInt(packageObject.label.substring(1)); const lastLabel = "v" + (labelNumber - 1); releaseSource = `Rolled back ${lastLabel} to ${packageObject.originalLabel}`; } if (releaseSource) { releaseTime += "\n" + chalk.magenta(`(${releaseSource})`).toString(); } let row = [packageObject.label, releaseTime, packageObject.appVersion, packageObject.isMandatory ? "Yes" : "No"]; if (command.displayAuthor) { let releasedBy = packageObject.releasedBy ? packageObject.releasedBy : ""; if (currentUserEmail && releasedBy === currentUserEmail) { releasedBy = "You"; } row.push(releasedBy); } row.push(packageObject.description ? wordwrap(30)(packageObject.description) : ""); row.push(getPackageMetricsString(packageObject) + (packageObject.isDisabled ? `\n${chalk.green("Disabled:")} Yes` : "")); if (packageObject.isDisabled) { row = row.map((cellContents) => applyChalkSkippingLineBreaks(cellContents, chalk.dim)); } dataSource.push(row); }); }); } } function applyChalkSkippingLineBreaks(applyString, chalkMethod) { // Used to prevent "chalk" from applying styles to linebreaks which // causes table border chars to have the style applied as well. return applyString .split("\n") .map((token) => chalkMethod(token)) .join("\n"); } function getPackageString(packageObject) { if (!packageObject) { return chalk.magenta("No updates released").toString(); } let packageString = chalk.green("Label: ") + packageObject.label + "\n" + chalk.green("App Version: ") + packageObject.appVersion + "\n" + chalk.green("Mandatory: ") + (packageObject.isMandatory ? "Yes" : "No") + "\n" + chalk.green("Release Time: ") + formatDate(packageObject.uploadTime) + "\n" + chalk.green("Released By: ") + (packageObject.releasedBy ? packageObject.releasedBy : "") + (packageObject.description ? wordwrap(70)("\n" + chalk.green("Description: ") + packageObject.description) : ""); if (packageObject.isDisabled) { packageString += `\n${chalk.green("Disabled:")} Yes`; } return packageString; } function getPackageMetricsString(obj) { const packageObject = obj; const rolloutString = obj && obj.rollout && obj.rollout !== 100 ? `\n${chalk.green("Rollout:")} ${obj.rollout.toLocaleString()}%` : ""; if (!packageObject || !packageObject.metrics) { return chalk.magenta("No installs recorded").toString() + (rolloutString || ""); } const activePercent = packageObject.metrics.totalActive ? (packageObject.metrics.active / packageObject.metrics.totalActive) * 100 : 0.0; let percentString; if (activePercent === 100.0) { percentString = "100%"; } else if (activePercent === 0.0) { percentString = "0%"; } else { percentString = activePercent.toPrecision(2) + "%"; } const numPending = packageObject.metrics.downloaded - packageObject.metrics.installed - packageObject.metrics.failed; let returnString = chalk.green("Active: ") + percentString + " (" + packageObject.metrics.active.toLocaleString() + " of " + packageObject.metrics.totalActive.toLocaleString() + ")\n" + chalk.green("Total: ") + packageObject.metrics.installed.toLocaleString(); if (numPending > 0) { returnString += " (" + numPending.toLocaleString() + " pending)"; } if (packageObject.metrics.failed) { returnString += "\n" + chalk.green("Rollbacks: ") + chalk.red(packageObject.metrics.failed.toLocaleString() + ""); } if (rolloutString) { returnString += rolloutString; } return returnString; } function getReactNativeProjectAppVersion(command, projectName) { (0, exports.log)(chalk.cyan(`Detecting ${command.platform} app version:\n`)); if (command.platform === "ios") { let resolvedPlistFile = command.plistFile; if (resolvedPlistFile) { // If a plist file path is explicitly provided, then we don't // need to attempt to "resolve" it within the well-known locations. if (!(0, file_utils_1.fileExists)(resolvedPlistFile)) { throw new Error("The specified plist file doesn't exist. Please check that the provided path is correct."); } } else { // Allow the plist prefix to be specified with or without a trailing // separator character, but prescribe the use of a hyphen when omitted, // since this is the most commonly used convetion for plist files. if (command.plistFilePrefix && /.+[^-.]$/.test(command.plistFilePrefix)) { command.plistFilePrefix += "-"; } const iOSDirectory = "ios"; const plistFileName = `${command.plistFilePrefix || ""}Info.plist`; const knownLocations = [path.join(iOSDirectory, projectName, plistFileName), path.join(iOSDirectory, plistFileName)]; resolvedPlistFile = knownLocations.find(file_utils_1.fileExists); if (!resolvedPlistFile) { throw new Error(`Unable to find either of the following plist files in order to infer your app's binary version: "${knownLocations.join('", "')}". If your plist has a different name, or is located in a different directory, consider using either the "--plistFile" or "--plistFilePrefix" parameters to help inform the CLI how to find it.`); } } const plistContents = fs.readFileSync(resolvedPlistFile).toString(); let parsedPlist; try { parsedPlist = plist.parse(plistContents); } catch (e) { throw new Error(`Unable to parse "${resolvedPlistFile}". Please ensure it is a well-formed plist file.`); } if (parsedPlist && parsedPlist.CFBundleShortVersionString) { if ((0, react_native_utils_1.isValidVersion)(parsedPlist.CFBundleShortVersionString)) { (0, exports.log)(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`); return Q(parsedPlist.CFBundleShortVersionString); } else { if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") { throw new Error(`The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); } return getAppVersionFromXcodeProject(command, projectName); } } else { throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`); } } else if (command.platform === "android") { let buildGradlePath = path.join("android", "app"); if (command.gradleFile) { buildGradlePath = command.gradleFile; } if (fs.lstatSync(buildGradlePath).isDirectory()) { buildGradlePath = path.join(buildGradlePath, "build.gradle"); } if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(buildGradlePath)) { throw new Error(`Unable to find gradle file "${buildGradlePath}".`); } return g2js .parseFile(buildGradlePath) .catch(() => { throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); }) .then((buildGradle) => { let versionName = null; // First 'if' statement was implemented as workaround for case // when 'build.gradle' file contains several 'android' nodes. // In this case 'buildGradle.android' prop represents array instead of object // due to parsing issue in 'g2js.parseFile' method. if (buildGradle.android instanceof Array) { for (let i = 0; i < buildGradle.android.length; i++) { const gradlePart = buildGradle.android[i]; if (gradlePart.defaultConfig && gradlePart.defaultConfig.versionName) { versionName = gradlePart.defaultConfig.versionName; break; } } } else if (buildGradle.android && buildGradle.android.defaultConfig && buildGradle.android.defaultConfig.versionName) { versionName = buildGradle.android.defaultConfig.versionName; } else { throw new Error(`The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`); } if (typeof versionName !== "string") { throw new Error(`The "android.defaultConfig.versionName" property value in "${buildGradlePath}" is not a valid string. If this is expected, consider using the --targetBinaryVersion option to specify the value manually.`); } let appVersion = versionName.replace(/"/g, "").trim(); if ((0, react_native_utils_1.isValidVersion)(appVersion)) { // The versionName property is a valid semver string, // so we can safely use that and move on. (0, exports.log)(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`); return appVersion; } else if (/^\d.*/.test(appVersion)) { // The versionName property isn't a valid semver string, // but it starts with a number, and therefore, it can't // be a valid Gradle property reference. throw new Error(`The "android.defaultConfig.versionName" property in the "${buildGradlePath}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); } // The version property isn't a valid semver string // so we assume it is a reference to a property variable. const propertyName = appVersion.replace("project.", ""); const propertiesFileName = "gradle.properties"; const knownLocations = [path.join("android", "app", propertiesFileName), path.join("android", propertiesFileName)]; // Search for gradle properties across all `gradle.properties` files let propertiesFile = null; for (let i = 0; i < knownLocations.length; i++) { propertiesFile = knownLocations[i]; if ((0, file_utils_1.fileExists)(propertiesFile)) { const propertiesContent = fs.readFileSync(propertiesFile).toString(); try { const parsedProperties = properties.parse(propertiesContent); appVersion = parsedProperties[propertyName]; if (appVersion) { break; } } catch (e) { throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`); } } } if (!appVersion) { throw new Error(`No property named "${propertyName}" exists in the "${propertiesFile}" file.`); } if (!(0, react_native_utils_1.isValidVersion)(appVersion)) { throw new Error(`The "${propertyName}" property in the "${propertiesFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); } (0, exports.log)(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`); return appVersion.toString(); }); } else { const appxManifestFileName = "Package.appxmanifest"; let appxManifestContainingFolder; let appxManifestContents; try { appxManifestContainingFolder = path.join("windows", projectName); appxManifestContents = fs.readFileSync(path.join(appxManifestContainingFolder, "Package.appxmanifest")).toString(); } catch (err) { throw new Error(`Unable to find or read "${appxManifestFileName}" in the "${path.join("windows", projectName)}" folder.`); } return parseXml(appxManifestContents) .catch((err) => { throw new Error(`Unable to parse the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file, it could be malformed.`); }) .then((parsedAppxManifest) => { try { return parsedAppxManifest.Package.Identity[0]["$"].Version.match(/^\d+\.\d+\.\d+/)[0]; } catch (e) { throw new Error(`Unable to parse the package version from the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file.`); } }); } } function getAppVersionFromXcodeProject(command, projectName) { const pbxprojFileName = "project.pbxproj"; let resolvedPbxprojFile = command.xcodeProjectFile; if (resolvedPbxprojFile) { // If the xcode project file path is explicitly provided, then we don't // need to attempt to "resolve" it within the well-known locations. if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) { // Specify path to pbxproj file if the provided file path is an Xcode project file. resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName); } if (!(0, file_utils_1.fileExists)(resolvedPbxprojFile)) { throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct."); } } else { const iOSDirectory = "ios"; const xcodeprojDirectory = `${projectName}.xcodeproj`; const pbxprojKnownLocations = [ path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName), path.join(iOSDirectory, pbxprojFileName), ]; resolvedPbxprojFile = pbxprojKnownLocations.find(file_utils_1.fileExists); if (!resolvedPbxprojFile) { throw new Error(`Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join('", "')}".`); } } const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync(); const marketingVersion = xcodeProj.getBuildProperty("MARKETING_VERSION", command.buildConfigurationName, command.xcodeTargetName); if (!(0, react_native_utils_1.isValidVersion)(marketingVersion)) { throw new Error(`The "MARKETING_VERSION" key in the "${resolvedPbxprojFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); } console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`); return marketingVersion; } function printJson(object) { (0, exports.log)(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); } function printAccessKeys(format, keys) { if (format === "json") { printJson(keys); } else if (format === "table") { printTable(["Name", "Created", "Expires"], (dataSource) => { const now = new Date().getTime(); function isExpired(key) { return now >= key.expires; } function keyToTableRow(key, dim) { const row = [key.name, key.createdTime ? formatDate(key.createdTime) : "", formatDate(key.expires)]; if (dim) { row.forEach((col, index) => { row[index] = chalk.dim(col); }); } return row; } keys.forEach((key) => !isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ false))); keys.forEach((key) => isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ true))); }); } } function printSessions(format, sessions) { if (format === "json") { printJson(sessions); } else if (format === "table") { printTable(["Machine", "Logged in"], (dataSource) => { sessions.forEach((session) => dataSource.push([session.machineName, formatDate(session.loggedInTime)])); }); } } function printTable(columnNames, readData) { const table = new Table({ head: columnNames, style: { head: ["cyan"] }, }); readData(table); (0, exports.log)(table.toString()); } function register(command) { return loginWithExternalAuthentication("register", command.serverUrl); } function promote(command) { const packageInfo = { appVersion: command.appStoreVersion, description: command.description, label: command.label, isDisabled: command.disabled, isMandatory: command.mandatory, rollout: command.rollout, }; return exports.sdk .promote(command.appName, command.sourceDeploymentName, command.destDeploymentName, packageInfo) .then(() => { (0, exports.log)("Successfully promoted " + (command.label !== null ? '"' + command.label + '" of ' : "") + 'the "' + command.sourceDeploymentName + '" deployment of the "' + command.appName + '" app to the "' + command.destDeploymentName + '" deployment.'); }) .catch((err) => releaseErrorHandler(err, command)); } function patch(command) { const packageInfo = { appVersion: command.appStoreVersion, description: command.description, isMandatory: command.mandatory, isDisabled: command.disabled, rollout: command.rollout, }; for (const updateProperty in packageInfo) { if (packageInfo[updateProperty] !== null) { return exports.sdk.patchRelease(command.appName, command.deploymentName, command.label, packageInfo).then(() => { (0, exports.log)(`Successfully updated the "${command.label ? command.label : `latest`}" release of "${command.appName}" app's "${command.deploymentName}" deployment.`); }); } } throw new Error("At least one property must be specified to patch a release."); } const release = (command) => { if ((0, file_utils_1.isBinaryOrZip)(command.package)) { throw new Error("It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."); } throwForInvalidSemverRange(command.appStoreVersion); const filePath = command.package; let isSingleFilePackage = true; if (fs.lstatSync(filePath).isDirectory()) { isSingleFilePackage = false; } let lastTotalProgress = 0; const progressBar = new progress("Upload progress:[:bar] :percent :etas", { complete: "=", incomplete: " ", width: 50, total: 100, }); const uploadProgress = (currentProgress) => { progressBar.tick(currentProgress - lastTotalProgress); lastTotalProgress = currentProgress; }; const updateMetadata = { description: command.description, isDisabled: command.disabled, isMandatory: command.mandatory, rollout: command.rollout, }; return exports.sdk .isAuthenticated(true) .then((isAuth) => { return exports.sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress); }) .then(() => { (0, exports.log)('Successfully released an update containing the "' + command.package + '" ' + (isSingleFilePackage ? "file" : "directory") + ' to the "' + command.deploymentName + '" deployment of the "' + command.appName + '" app.'); }) .catch((err) => releaseErrorHandler(err, command)); }; exports.release = release; const releaseReact = (command) => { let bundleName = command.bundleName; let entryFile = command.entryFile; const outputFolder = command.outputDir || path.join(os.tmpdir(), "CodePush"); const platform = (command.platform = command.platform.toLowerCase()); const releaseCommand = command; // Check for app and deployment exist before releasing an update. // This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name. return (exports.sdk .getDeployment(command.appName, command.deploymentName) .then(() => { releaseCommand.package = outputFolder; switch (platform) { case "android": case "ios": case "windows": if (!bundleName) { bundleName = platform === "ios" ? "main.jsbundle" : `index.${platform}.bundle`; } break; default: throw new Error('Platform must be either "android" or "ios".'); } let projectName; try { const projectPackageJson = require(path.join(process.cwd(), "package.json")); projectName = projectPackageJson.name; if (!projectName) { throw new Error('The "package.json" file in the CWD does not have the "name" field set.'); } if (!projectPackageJson.dependencies["react-native"]) { throw new Error("The project in the CWD is not a React Native project."); } } catch (error) { throw new Error('Unable to find or read "package.json" in the CWD. The "release-react" command must be executed in a React Native project folder.'); } if (!entryFile) { entryFile = `index.${platform}.js`; if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) { entryFile = "index.js"; } if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) { throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`); } } else { if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) { throw new Error(`Entry file "${entryFile}" does not exist.`); } } const appVersionPromise = command.appStoreVersion ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) { command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map"); } return appVersionPromise; }) .then((appVersion) => { throwForInvalidSemverRange(appVersion); releaseCommand.appStoreVersion = appVersion; return (0, exports.createEmptyTempReleaseFolder)(outputFolder); }) // This is needed to clear the react native bundler cache: // https://github.com/facebook/react-native/issues/4289 .then(() => deleteFolder(`${os.tmpdir()}/react-*`)) .then(() => (0, exports.runReactNativeBundleCommand)(bundleName, command.development || false, entryFile, outputFolder, platform, command.sourcemapOutput)) .then(async () => { const isHermesEnabled = command.useHermes || (platform === "android" && (await (0, react_native_utils_1.getAndroidHermesEnabled)(command.gradleFile))) || // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in build.gradle and we're releasing an Android build (platform === "ios" && (await (0, react_native_utils_1.getiOSHermesEnabled)(command.podFile))); // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build if (isHermesEnabled) { (0, exports.log)(chalk.cyan("\nRunning hermes compiler...\n")); await (0, react_native_utils_1.runHermesEmitBinaryCommand)(bundleName, outputFolder, command.sourcemapOutput, command.extraHermesFlags, command.gradleFile); } }) .then(async () => { if (command.privateKeyPath) { (0, exports.log)(chalk.cyan("\nSigning the bundle:\n")); await (0, sign_1.default)(command.privateKeyPath, outputFolder); } else { console.log("private key was not provided"); } }) .then(() => { (0, exports.log)(chalk.cyan("\nReleasing update contents to CodePush:\n")); return (0, exports.release)(releaseCommand); }) .then(() => { if (!command.outputDir) { deleteFolder(outputFolder); } }) .catch((err) => { deleteFolder(outputFolder); throw err; })); }; exports.releaseReact = releaseReact; function rollback(command) {