microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6b8cdbb830ed7d91525cc76810df5458772836e7

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

lib/codepush-node-sdk/src/react-native/react-native-utils.ts

360lines · modecode

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