microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
src/extension/appcenter/lib/codepush-node-sdk/dist/react-native/react-native-utils.js
326lines · modecode
| 1 | "use strict"; |
| 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { |
| 3 | return new (P || (P = Promise))(function (resolve, reject) { |
| 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } |
| 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } |
| 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } |
| 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); |
| 8 | }); |
| 9 | }; |
| 10 | Object.defineProperty(exports, "__esModule", { value: true }); |
| 11 | const fs = require("fs"); |
| 12 | const path = require("path"); |
| 13 | const chalk_1 = require("chalk"); |
| 14 | const xml2js = require("xml2js"); |
| 15 | const mkdirp = require("mkdirp"); |
| 16 | const plist = require('plist'); |
| 17 | const g2js = require('gradle-to-js/lib/parser'); |
| 18 | const properties = require('properties'); |
| 19 | const childProcess = require('child_process'); |
| 20 | const validation_utils_1 = require("../utils/validation-utils"); |
| 21 | const fileUtils = require("../utils/file-utils"); |
| 22 | exports.spawn = childProcess.spawn; |
| 23 | function getAndroidAppVersion(projectRoot, gradleFile) { |
| 24 | return __awaiter(this, void 0, void 0, function* () { |
| 25 | projectRoot = projectRoot || process.cwd(); |
| 26 | const projectPackageJson = require(path.join(projectRoot, 'package.json')); |
| 27 | const projectName = projectPackageJson.name; |
| 28 | console.log(chalk_1.default.cyan(`Detecting "Android" app version:\n`)); |
| 29 | let buildGradlePath = path.join(projectRoot, 'android', 'app'); |
| 30 | if (gradleFile) { |
| 31 | buildGradlePath = gradleFile; |
| 32 | } |
| 33 | if (fs.lstatSync(buildGradlePath).isDirectory()) { |
| 34 | buildGradlePath = path.join(buildGradlePath, 'build.gradle'); |
| 35 | } |
| 36 | if (fileUtils.fileDoesNotExistOrIsDirectory(buildGradlePath)) { |
| 37 | throw new Error(`Unable to find gradle file "${buildGradlePath}".`); |
| 38 | } |
| 39 | return g2js.parseFile(buildGradlePath) |
| 40 | .catch(() => { |
| 41 | throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); |
| 42 | }) |
| 43 | .then((buildGradle) => { |
| 44 | let versionName = null; |
| 45 | // First 'if' statement was implemented as workaround for case |
| 46 | // when 'build.gradle' file contains several 'android' nodes. |
| 47 | // In this case 'buildGradle.android' prop represents array instead of object |
| 48 | // due to parsing issue in 'g2js.parseFile' method. |
| 49 | if (buildGradle.android instanceof Array) { |
| 50 | for (let i = 0; i < buildGradle.android.length; i++) { |
| 51 | const gradlePart = buildGradle.android[i]; |
| 52 | if (gradlePart.defaultConfig && gradlePart.defaultConfig.versionName) { |
| 53 | versionName = gradlePart.defaultConfig.versionName; |
| 54 | break; |
| 55 | } |
| 56 | } |
| 57 | } |
| 58 | else if (buildGradle.android && buildGradle.android.defaultConfig && buildGradle.android.defaultConfig.versionName) { |
| 59 | versionName = buildGradle.android.defaultConfig.versionName; |
| 60 | } |
| 61 | else { |
| 62 | throw new Error(`The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`); |
| 63 | } |
| 64 | if (typeof versionName !== 'string') { |
| 65 | 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.`); |
| 66 | } |
| 67 | let appVersion = versionName.replace(/"/g, '').trim(); |
| 68 | if (validation_utils_1.isValidVersion(appVersion)) { |
| 69 | // The versionName property is a valid semver string, |
| 70 | // so we can safely use that and move on. |
| 71 | console.log(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`); |
| 72 | return appVersion; |
| 73 | } |
| 74 | // The version property isn't a valid semver string |
| 75 | // so we assume it is a reference to a property variable. |
| 76 | const propertyName = appVersion.replace('project.', ''); |
| 77 | const propertiesFileName = 'gradle.properties'; |
| 78 | const knownLocations = [ |
| 79 | path.join(projectRoot, 'android', 'app', propertiesFileName), |
| 80 | path.join(projectRoot, 'android', propertiesFileName) |
| 81 | ]; |
| 82 | // Search for gradle properties across all `gradle.properties` files |
| 83 | let propertiesFile = null; |
| 84 | for (let i = 0; i < knownLocations.length; i++) { |
| 85 | propertiesFile = knownLocations[i]; |
| 86 | if (fileUtils.fileExists(propertiesFile)) { |
| 87 | const propertiesContent = fs.readFileSync(propertiesFile).toString(); |
| 88 | try { |
| 89 | const parsedProperties = properties.parse(propertiesContent); |
| 90 | appVersion = parsedProperties[propertyName]; |
| 91 | if (appVersion) { |
| 92 | break; |
| 93 | } |
| 94 | } |
| 95 | catch (e) { |
| 96 | throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`); |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | if (!appVersion) { |
| 101 | throw new Error(`No property named "${propertyName}" exists in the "${propertiesFile}" file.`); |
| 102 | } |
| 103 | if (!validation_utils_1.isValidVersion(appVersion)) { |
| 104 | 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).`); |
| 105 | } |
| 106 | console.log(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`); |
| 107 | return appVersion.toString(); |
| 108 | }); |
| 109 | }); |
| 110 | } |
| 111 | exports.getAndroidAppVersion = getAndroidAppVersion; |
| 112 | function getiOSAppVersion(projectRoot, plistFilePrefix, plistFile) { |
| 113 | return __awaiter(this, void 0, void 0, function* () { |
| 114 | projectRoot = projectRoot || process.cwd(); |
| 115 | const projectPackageJson = require(path.join(projectRoot, 'package.json')); |
| 116 | const projectName = projectPackageJson.name; |
| 117 | console.log(chalk_1.default.cyan(`Detecting "iOS" app version:\n`)); |
| 118 | let resolvedPlistFile = plistFile; |
| 119 | if (resolvedPlistFile) { |
| 120 | // If a plist file path is explicitly provided, then we don't |
| 121 | // need to attempt to "resolve" it within the well-known locations. |
| 122 | if (!fileUtils.fileExists(resolvedPlistFile)) { |
| 123 | throw new Error(`The specified plist file doesn't exist. Please check that the provided path is correct.`); |
| 124 | } |
| 125 | } |
| 126 | else { |
| 127 | // Allow the plist prefix to be specified with or without a trailing |
| 128 | // separator character, but prescribe the use of a hyphen when omitted, |
| 129 | // since this is the most commonly used convetion for plist files. |
| 130 | if (plistFilePrefix && /.+[^-.]$/.test(plistFilePrefix)) { |
| 131 | plistFilePrefix += '-'; |
| 132 | } |
| 133 | const iOSDirectory = 'ios'; |
| 134 | const plistFileName = `${plistFilePrefix || ''}Info.plist`; |
| 135 | const knownLocations = [ |
| 136 | path.join(projectRoot, iOSDirectory, projectName, plistFileName), |
| 137 | path.join(projectRoot, iOSDirectory, plistFileName) |
| 138 | ]; |
| 139 | resolvedPlistFile = knownLocations.find(fileUtils.fileExists); |
| 140 | if (!resolvedPlistFile) { |
| 141 | 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.`); |
| 142 | } |
| 143 | } |
| 144 | const plistContents = fs.readFileSync(resolvedPlistFile).toString(); |
| 145 | let parsedPlist; |
| 146 | try { |
| 147 | parsedPlist = plist.parse(plistContents); |
| 148 | } |
| 149 | catch (e) { |
| 150 | throw new Error(`Unable to parse "${resolvedPlistFile}". Please ensure it is a well-formed plist file.`); |
| 151 | } |
| 152 | if (parsedPlist && parsedPlist.CFBundleShortVersionString) { |
| 153 | if (validation_utils_1.isValidVersion(parsedPlist.CFBundleShortVersionString)) { |
| 154 | console.log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`); |
| 155 | return Promise.resolve(parsedPlist.CFBundleShortVersionString); |
| 156 | } |
| 157 | else { |
| 158 | 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).`); |
| 159 | } |
| 160 | } |
| 161 | else { |
| 162 | throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`); |
| 163 | } |
| 164 | }); |
| 165 | } |
| 166 | exports.getiOSAppVersion = getiOSAppVersion; |
| 167 | function getWindowsAppVersion(projectRoot) { |
| 168 | return __awaiter(this, void 0, void 0, function* () { |
| 169 | projectRoot = projectRoot || process.cwd(); |
| 170 | const projectPackageJson = require(path.join(projectRoot, 'package.json')); |
| 171 | const projectName = projectPackageJson.name; |
| 172 | console.log(chalk_1.default.cyan(`Detecting "Windows" app version:\n`)); |
| 173 | const appxManifestFileName = 'Package.appxmanifest'; |
| 174 | let appxManifestContainingFolder; |
| 175 | let appxManifestContents; |
| 176 | try { |
| 177 | appxManifestContainingFolder = path.join(projectRoot, 'windows', projectName); |
| 178 | appxManifestContents = fs.readFileSync(path.join(appxManifestContainingFolder, appxManifestFileName)).toString(); |
| 179 | } |
| 180 | catch (err) { |
| 181 | throw new Error(`Unable to find or read "${appxManifestFileName}" in the "${path.join('windows', projectName)}" folder.`); |
| 182 | } |
| 183 | return new Promise((resolve, reject) => { |
| 184 | xml2js.parseString(appxManifestContents, (err, parsedAppxManifest) => { |
| 185 | if (err) { |
| 186 | reject(new Error(`Unable to parse the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file, it could be malformed.`)); |
| 187 | return; |
| 188 | } |
| 189 | try { |
| 190 | const appVersion = parsedAppxManifest.Package.Identity[0]['$'].Version.match(/^\d+\.\d+\.\d+/)[0]; |
| 191 | console.log(`Using the target binary version value "${appVersion}" from the "Identity" key in the "${appxManifestFileName}" file.\n`); |
| 192 | return resolve(appVersion); |
| 193 | } |
| 194 | catch (e) { |
| 195 | reject(new Error(`Unable to parse the package version from the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file.`)); |
| 196 | return; |
| 197 | } |
| 198 | }); |
| 199 | }); |
| 200 | }); |
| 201 | } |
| 202 | exports.getWindowsAppVersion = getWindowsAppVersion; |
| 203 | function runReactNativeBundleCommand(projectRootPath, bundleName, development, entryFile, outputFolder, platform, sourcemapOutput) { |
| 204 | const reactNativeBundleArgs = []; |
| 205 | const envNodeArgs = process.env.CODE_PUSH_NODE_ARGS; |
| 206 | if (typeof envNodeArgs !== 'undefined') { |
| 207 | Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/)); |
| 208 | } |
| 209 | Array.prototype.push.apply(reactNativeBundleArgs, [ |
| 210 | path.join(projectRootPath, 'node_modules', 'react-native', 'local-cli', 'cli.js'), 'bundle', |
| 211 | '--assets-dest', outputFolder, |
| 212 | '--bundle-output', path.join(outputFolder, bundleName), |
| 213 | '--dev', development, |
| 214 | '--entry-file', entryFile, |
| 215 | '--platform', platform, |
| 216 | ]); |
| 217 | if (sourcemapOutput) { |
| 218 | reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput); |
| 219 | } |
| 220 | console.log(chalk_1.default.cyan(`Running "react-native bundle" command:\n`)); |
| 221 | const reactNativeBundleProcess = exports.spawn('node', reactNativeBundleArgs); |
| 222 | console.log(`node ${reactNativeBundleArgs.join(' ')}`); |
| 223 | return new Promise((resolve, reject) => { |
| 224 | reactNativeBundleProcess.stdout.on('data', (data) => { |
| 225 | console.log(data.toString().trim()); |
| 226 | }); |
| 227 | reactNativeBundleProcess.stderr.on('data', (data) => { |
| 228 | console.error(data.toString().trim()); |
| 229 | }); |
| 230 | reactNativeBundleProcess.on('close', (exitCode) => { |
| 231 | if (exitCode) { |
| 232 | reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`)); |
| 233 | } |
| 234 | resolve(null); |
| 235 | }); |
| 236 | }); |
| 237 | } |
| 238 | exports.runReactNativeBundleCommand = runReactNativeBundleCommand; |
| 239 | function isValidOS(os) { |
| 240 | switch (os.toLowerCase()) { |
| 241 | case 'android': |
| 242 | case 'ios': |
| 243 | case 'windows': |
| 244 | return true; |
| 245 | default: |
| 246 | return false; |
| 247 | } |
| 248 | } |
| 249 | exports.isValidOS = isValidOS; |
| 250 | function isValidPlatform(platform) { |
| 251 | return platform.toLowerCase() === 'react-native'; |
| 252 | } |
| 253 | exports.isValidPlatform = isValidPlatform; |
| 254 | function isReactNativeProject() { |
| 255 | try { |
| 256 | const projectPackageJson = require(path.join(process.cwd(), 'package.json')); |
| 257 | const projectName = projectPackageJson.name; |
| 258 | if (!projectName) { |
| 259 | throw new Error(`The "package.json" file in the CWD does not have the "name" field set.`); |
| 260 | } |
| 261 | return projectPackageJson.dependencies['react-native'] || (projectPackageJson.devDependencies && projectPackageJson.devDependencies['react-native']); |
| 262 | } |
| 263 | catch (error) { |
| 264 | 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.`); |
| 265 | } |
| 266 | } |
| 267 | exports.isReactNativeProject = isReactNativeProject; |
| 268 | function getDefaultBundleName(os) { |
| 269 | if (!isValidOS(os)) { |
| 270 | throw new Error(`Platform must be either "ios" or "android".`); |
| 271 | } |
| 272 | return os === 'ios' |
| 273 | ? 'main.jsbundle' |
| 274 | : `index.${os}.bundle`; |
| 275 | } |
| 276 | exports.getDefaultBundleName = getDefaultBundleName; |
| 277 | function getDefautEntryFilePath(os, projectDir) { |
| 278 | if (!isValidOS(os)) { |
| 279 | throw new Error(`Platform must be either "ios" or "android".`); |
| 280 | } |
| 281 | if (!projectDir) { |
| 282 | projectDir = process.cwd(); |
| 283 | } |
| 284 | let entryFilePath = path.join(projectDir, `index.${os}.js`); |
| 285 | if (fileUtils.fileDoesNotExistOrIsDirectory(path.join(projectDir, entryFilePath))) { |
| 286 | entryFilePath = path.join(projectDir, 'index.js'); |
| 287 | } |
| 288 | if (fileUtils.fileDoesNotExistOrIsDirectory(entryFilePath)) { |
| 289 | throw new Error(`Entry file "index.${os}.js" or "index.js" does not exist.`); |
| 290 | } |
| 291 | return entryFilePath; |
| 292 | } |
| 293 | exports.getDefautEntryFilePath = getDefautEntryFilePath; |
| 294 | class BundleConfig { |
| 295 | } |
| 296 | exports.BundleConfig = BundleConfig; |
| 297 | function makeUpdateContents(bundleConfig) { |
| 298 | return __awaiter(this, void 0, void 0, function* () { |
| 299 | if (!isValidOS(bundleConfig.os)) { |
| 300 | throw new Error(`Platform must be either "ios" or "android".`); |
| 301 | } |
| 302 | if (!bundleConfig.projectRootPath) { |
| 303 | bundleConfig.projectRootPath = process.cwd(); |
| 304 | } |
| 305 | let updateContentsPath; |
| 306 | updateContentsPath = bundleConfig.outputDir || (yield fileUtils.mkTempDir('code-push')); |
| 307 | // we have to add "CodePush" root folder to make update contents file structure |
| 308 | // to be compatible with React Native client SDK |
| 309 | updateContentsPath = path.join(updateContentsPath, 'CodePush'); |
| 310 | mkdirp.sync(updateContentsPath); |
| 311 | if (!bundleConfig.bundleName) { |
| 312 | bundleConfig.bundleName = getDefaultBundleName(bundleConfig.os); |
| 313 | } |
| 314 | if (!bundleConfig.entryFilePath) { |
| 315 | bundleConfig.entryFilePath = getDefautEntryFilePath(bundleConfig.os, bundleConfig.projectRootPath); |
| 316 | } |
| 317 | if (bundleConfig.outputDir) { |
| 318 | bundleConfig.sourcemapOutput = path.join(updateContentsPath, bundleConfig.bundleName + '.map'); |
| 319 | } |
| 320 | fileUtils.createEmptyTmpReleaseFolder(updateContentsPath); |
| 321 | fileUtils.removeReactTmpDir(); |
| 322 | yield runReactNativeBundleCommand(bundleConfig.projectRootPath, bundleConfig.bundleName, bundleConfig.development, bundleConfig.entryFilePath, updateContentsPath, bundleConfig.os, bundleConfig.sourcemapOutput); |
| 323 | return updateContentsPath; |
| 324 | }); |
| 325 | } |
| 326 | exports.makeUpdateContents = makeUpdateContents; |
| 327 | |