in src/commands/codepush/lib/react-native-utils.ts [33:290]
export async function getReactNativeProjectAppVersion(
versionSearchParams: VersionSearchParams,
projectRoot?: string
): Promise<string> {
projectRoot = projectRoot || process.cwd();
// eslint-disable-next-line security/detect-non-literal-require
const projectPackageJson: any = require(path.join(projectRoot, "package.json"));
const projectName: string = projectPackageJson.name;
const fileExists = (file: string): boolean => {
try {
return fs.statSync(file).isFile();
} catch (e) {
return false;
}
};
out.text(chalk.cyan(`Detecting ${versionSearchParams.os} app version:\n`));
if (versionSearchParams.os === "ios") {
let resolvedPlistFile: string = versionSearchParams.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 (!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 (versionSearchParams.plistFilePrefix && /.+[^-.]$/.test(versionSearchParams.plistFilePrefix)) {
versionSearchParams.plistFilePrefix += "-";
}
const iOSDirectory: string = "ios";
const plistFileName = `${versionSearchParams.plistFilePrefix || ""}Info.plist`;
const knownLocations = [path.join(iOSDirectory, projectName, plistFileName), path.join(iOSDirectory, plistFileName)];
resolvedPlistFile = (knownLocations as any).find(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 "--plist-file" or "--plist-file-prefix" parameters to help inform the CLI how to find it.`
);
}
}
const plistContents = fs.readFileSync(resolvedPlistFile).toString();
let parsedPlist: any;
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 (isValidVersion(parsedPlist.CFBundleShortVersionString)) {
out.text(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
return Promise.resolve(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).`
);
}
const pbxprojFileName = "project.pbxproj";
let resolvedPbxprojFile: string = versionSearchParams.projectFile;
if (resolvedPbxprojFile) {
// If a plist 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 (!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(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();
// If the build configuration name is not defined in the release-command, then "Release" build configuration is used by default.
const buildConfigurationName = versionSearchParams.buildConfigurationName;
const configsObj: XCBuildConfiguration = xcodeProj.getBuildConfigByName(buildConfigurationName);
if (Object.keys(configsObj).length === 0) {
throw new Error(`Unable to find the build configuration with "${buildConfigurationName}" name.`);
}
const marketingVersion = Object.values(configsObj).find((c) => c.buildSettings["MARKETING_VERSION"]).buildSettings
.MARKETING_VERSION;
if (!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).`
);
}
out.text(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`);
return Promise.resolve(marketingVersion);
}
} else {
throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
}
} else if (versionSearchParams.os === "android") {
let buildGradlePath: string = path.join("android", "app");
if (versionSearchParams.gradleFile) {
buildGradlePath = versionSearchParams.gradleFile;
}
if (fs.lstatSync(buildGradlePath).isDirectory()) {
buildGradlePath = path.join(buildGradlePath, "build.gradle");
}
if (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: any) => {
let versionName: string = 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 --target-binary-version option to specify the value manually.`
);
}
let appVersion: string = versionName.replace(/"/g, "").trim();
if (isValidVersion(appVersion)) {
// The versionName property is a valid semver string,
// so we can safely use that and move on.
out.text(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`);
return appVersion;
}
// 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: string = null;
for (let i = 0; i < knownLocations.length; i++) {
propertiesFile = knownLocations[i];
if (fileExists(propertiesFile)) {
const propertiesContent: string = fs.readFileSync(propertiesFile).toString();
try {
const parsedProperties: any = 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 (!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).`
);
}
out.text(
`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`
);
return appVersion.toString();
});
} else {
const appxManifestFileName: string = "Package.appxmanifest";
let appxManifestContents: string;
let appxManifestContainingFolder: string;
try {
appxManifestContainingFolder = path.join("windows", projectName);
appxManifestContents = fs.readFileSync(path.join(appxManifestContainingFolder, appxManifestFileName)).toString();
} catch (err) {
throw new Error(`Unable to find or read "${appxManifestFileName}" in the "${path.join("windows", projectName)}" folder.`);
}
return new Promise<string>((resolve, reject) => {
xml2js.parseString(appxManifestContents, (err: Error, parsedAppxManifest: any) => {
if (err) {
reject(
new Error(
`Unable to parse the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file, it could be malformed.`
)
);
return;
}
try {
const appVersion: string = parsedAppxManifest.Package.Identity[0]["$"].Version.match(/^\d+\.\d+\.\d+/)[0];
out.text(
`Using the target binary version value "${appVersion}" from the "Identity" key in the "${appxManifestFileName}" file.\n`
);
return resolve(appVersion);
} catch (e) {
reject(
new Error(
`Unable to parse the package version from the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file.`
)
);
return;
}
});
});
}
}