import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import * as xml2js from 'xml2js';
import * as mkdirp from 'mkdirp';
const plist = require('plist');
const g2js = require('gradle-to-js/lib/parser');
const properties = require('properties');
const childProcess = require('child_process');
import { isValidVersion } from '../utils/validation-utils';
import * as fileUtils from '../utils/file-utils';
export var spawn = childProcess.spawn;
export interface VersionSearchParams {
os: string; // ios or android
plistFile: string;
plistFilePrefix: string;
gradleFile: string;
}
export async function getAndroidAppVersion(projectRoot?: string, gradleFile?: string): Promise<string> {
projectRoot = projectRoot || process.cwd();
const projectPackageJson: any = require(path.join(projectRoot, 'package.json'));
const projectName: string = projectPackageJson.name;
console.log(chalk.cyan(`Detecting "Android" app version:\n`));
let buildGradlePath: string = path.join(projectRoot, 'android', 'app');
if (gradleFile) {
buildGradlePath = gradleFile;
}
if (fs.lstatSync(buildGradlePath).isDirectory()) {
buildGradlePath = path.join(buildGradlePath, 'build.gradle');
}
if (fileUtils.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.
console.log(`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(projectRoot, 'android', 'app', propertiesFileName),
path.join(projectRoot, '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 (fileUtils.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).`);
}
console.log(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`);
return appVersion.toString();
});
}
export async function getiOSAppVersion(projectRoot?: string, plistFilePrefix?: string, plistFile?: string): Promise<string> {
projectRoot = projectRoot || process.cwd();
const projectPackageJson: any = require(path.join(projectRoot, 'package.json'));
const projectName: string = projectPackageJson.name;
console.log(chalk.cyan(`Detecting "iOS" app version:\n`));
let resolvedPlistFile: string = 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 (!fileUtils.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 (plistFilePrefix && /.+[^-.]$/.test(plistFilePrefix)) {
plistFilePrefix += '-';
}
const iOSDirectory: string = 'ios';
const plistFileName = `${plistFilePrefix || ''}Info.plist`;
const knownLocations = [
path.join(projectRoot, iOSDirectory, projectName, plistFileName),
path.join(projectRoot, iOSDirectory, plistFileName)
];
resolvedPlistFile = (<any>knownLocations).find(fileUtils.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)) {
console.log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
return Promise.resolve(parsedPlist.CFBundleShortVersionString);
} else {
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).`);
}
} else {
throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
}
}
export async function getWindowsAppVersion(projectRoot?: string): Promise<string> {
projectRoot = projectRoot || process.cwd();
const projectPackageJson: any = require(path.join(projectRoot, 'package.json'));
const projectName: string = projectPackageJson.name;
console.log(chalk.cyan(`Detecting "Windows" app version:\n`));
const appxManifestFileName: string = 'Package.appxmanifest';
let appxManifestContainingFolder: string;
let appxManifestContents: string;
try {
appxManifestContainingFolder = path.join(projectRoot, '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];
console.log(`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;
}
});
});
}
export function runReactNativeBundleCommand(projectRootPath: string, bundleName: string, development: boolean, entryFile: string, outputFolder: string, platform: string, sourcemapOutput: string): Promise<void> {
const reactNativeBundleArgs: string[] = [];
const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
if (typeof envNodeArgs !== 'undefined') {
Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/));
}
Array.prototype.push.apply(reactNativeBundleArgs, [
path.join(projectRootPath, 'node_modules', 'react-native', 'local-cli', 'cli.js'), 'bundle',
'--assets-dest', outputFolder,
'--bundle-output', path.join(outputFolder, bundleName),
'--dev', development,
'--entry-file', entryFile,
'--platform', platform,
]);
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
console.log(chalk.cyan(`Running "react-native bundle" command:\n`));
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
console.log(`node ${reactNativeBundleArgs.join(' ')}`);
return new Promise<void>((resolve, reject) => {
reactNativeBundleProcess.stdout.on('data', (data: Buffer) => {
console.log(data.toString().trim());
});
reactNativeBundleProcess.stderr.on('data', (data: Buffer) => {
console.error(data.toString().trim());
});
reactNativeBundleProcess.on('close', (exitCode: number) => {
if (exitCode) {
reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
}
resolve(<void>null);
});
});
}
export function isValidOS(os: string): boolean {
switch (os.toLowerCase()) {
case 'android':
case 'ios':
case 'windows':
return true;
default:
return false;
}
}
export function isValidPlatform(platform: string): boolean {
return platform.toLowerCase() === 'react-native';
}
export function isReactNativeProject(): boolean {
try {
const projectPackageJson: any = require(path.join(process.cwd(), 'package.json'));
const projectName: string = projectPackageJson.name;
if (!projectName) {
throw new Error(`The "package.json" file in the CWD does not have the "name" field set.`);
}
return projectPackageJson.dependencies['react-native'] || (projectPackageJson.devDependencies && projectPackageJson.devDependencies['react-native']);
} 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.`);
}
}
export function getDefaultBundleName(os: string): string {
if (!isValidOS(os)) {
throw new Error(`Platform must be either "ios" or "android".`);
}
return os === 'ios'
? 'main.jsbundle'
: `index.${os}.bundle`;
}
export function getDefautEntryFilePath(os: string, projectDir?: string): string {
if (!isValidOS(os)) {
throw new Error(`Platform must be either "ios" or "android".`);
}
if (!projectDir) {
projectDir = process.cwd();
}
let entryFilePath: string = path.join(projectDir, `index.${os}.js`);
if (fileUtils.fileDoesNotExistOrIsDirectory(path.join(projectDir, entryFilePath))) {
entryFilePath = path.join(projectDir, 'index.js');
}
if (fileUtils.fileDoesNotExistOrIsDirectory(entryFilePath)) {
throw new Error(`Entry file "index.${os}.js" or "index.js" does not exist.`);
}
return entryFilePath;
}
export class BundleConfig {
os: string;
projectRootPath: string;
outputDir?: string;
entryFilePath?: string;
bundleName?: string;
development?: boolean;
sourcemapOutput?: string;
}
export async function makeUpdateContents(bundleConfig: BundleConfig): Promise<string> {
if (!isValidOS(bundleConfig.os)) {
throw new Error(`Platform must be either "ios" or "android".`);
}
if (!bundleConfig.projectRootPath) {
bundleConfig.projectRootPath = process.cwd();
}
let updateContentsPath: string;
updateContentsPath = bundleConfig.outputDir || await fileUtils.mkTempDir('code-push');
// we have to add "CodePush" root folder to make update contents file structure
// to be compatible with React Native client SDK
updateContentsPath = path.join(updateContentsPath, 'CodePush');
mkdirp.sync(updateContentsPath);
if (!bundleConfig.bundleName) {
bundleConfig.bundleName = getDefaultBundleName(bundleConfig.os);
}
if (!bundleConfig.entryFilePath) {
bundleConfig.entryFilePath = getDefautEntryFilePath(bundleConfig.os, bundleConfig.projectRootPath);
}
if (bundleConfig.outputDir) {
bundleConfig.sourcemapOutput = path.join(updateContentsPath, bundleConfig.bundleName + '.map');
}
fileUtils.createEmptyTmpReleaseFolder(updateContentsPath);
fileUtils.removeReactTmpDir();
await runReactNativeBundleCommand(bundleConfig.projectRootPath, bundleConfig.bundleName, bundleConfig.development, bundleConfig.entryFilePath, updateContentsPath, bundleConfig.os, bundleConfig.sourcemapOutput);
return updateContentsPath;
}microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
lib/codepush-node-sdk/src/react-native/react-native-utils.ts
360lines · modepreview