diff --git a/package.json b/package.json index 03ae7b7d0..b253dca1f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ ] }, "commands": { - "postlink": "node node_modules/react-native-code-push/scripts/postlink/run" + "postlink": "node node_modules/react-native-code-push/scripts/postlink/run", + "postunlink": "node node_modules/react-native-code-push/scripts/postunlink/run" } } } diff --git a/scripts/postlink/android/postlink.js b/scripts/postlink/android/postlink.js index ecf280bc8..3102afd36 100644 --- a/scripts/postlink/android/postlink.js +++ b/scripts/postlink/android/postlink.js @@ -1,70 +1,37 @@ +var linkTools = require('../../tools/linkToolsAndroid'); var fs = require("fs"); -var glob = require("glob"); -var path = require("path"); var inquirer = require('inquirer'); module.exports = () => { console.log("Running android postlink script"); - var ignoreFolders = { ignore: ["node_modules/**", "**/build/**"] }; - var buildGradlePath = path.join("android", "app", "build.gradle"); - var manifestPath = glob.sync("**/AndroidManifest.xml", ignoreFolders)[0]; - - function findMainApplication() { - if (!manifestPath) { - return null; - } - - var manifest = fs.readFileSync(manifestPath, "utf8"); - - // Android manifest must include single 'application' element - var matchResult = manifest.match(/application\s+android:name\s*=\s*"(.*?)"/); - if (matchResult) { - var appName = matchResult[1]; - } else { - return null; - } - - var nameParts = appName.split('.'); - var searchPath = glob.sync("**/" + nameParts[nameParts.length - 1] + ".java", ignoreFolders)[0]; - return searchPath; - } - - var mainApplicationPath = findMainApplication() || glob.sync("**/MainApplication.java", ignoreFolders)[0]; + var buildGradlePath = linkTools.getBuildGradlePath(); + var mainApplicationPath = linkTools.getMainApplicationLocation(); // 1. Add the getJSBundleFile override - var getJSBundleFileOverride = ` - @Override - protected String getJSBundleFile() { - return CodePush.getJSBundleFile(); - } - `; - - function isAlreadyOverridden(codeContents) { - return /@Override\s*\n\s*protected String getJSBundleFile\(\)\s*\{[\s\S]*?\}/.test(codeContents); - } + var getJSBundleFileOverride = linkTools.getJSBundleFileOverride; if (mainApplicationPath) { var mainApplicationContents = fs.readFileSync(mainApplicationPath, "utf8"); - if (isAlreadyOverridden(mainApplicationContents)) { + if (linkTools.isJsBundleOverridden(mainApplicationContents)) { console.log(`"getJSBundleFile" is already overridden`); } else { - var reactNativeHostInstantiation = "new ReactNativeHost(this) {"; + var reactNativeHostInstantiation = linkTools.reactNativeHostInstantiation; mainApplicationContents = mainApplicationContents.replace(reactNativeHostInstantiation, - `${reactNativeHostInstantiation}\n${getJSBundleFileOverride}`); + `${reactNativeHostInstantiation}${getJSBundleFileOverride}`); fs.writeFileSync(mainApplicationPath, mainApplicationContents); } } else { - var mainActivityPath = glob.sync("**/MainActivity.java", ignoreFolders)[0]; + var mainActivityPath = linkTools.getMainActivityPath(); if (mainActivityPath) { var mainActivityContents = fs.readFileSync(mainActivityPath, "utf8"); - if (isAlreadyOverridden(mainActivityContents)) { + if (linkTools.isJsBundleOverridden(mainActivityContents)) { console.log(`"getJSBundleFile" is already overridden`); } else { - var mainActivityClassDeclaration = "public class MainActivity extends ReactActivity {"; + var mainActivityClassDeclaration = linkTools.mainActivityClassDeclaration; mainActivityContents = mainActivityContents.replace(mainActivityClassDeclaration, - `${mainActivityClassDeclaration}\n${getJSBundleFileOverride}`); + `${mainActivityClassDeclaration}${getJSBundleFileOverride}`); fs.writeFileSync(mainActivityPath, mainActivityContents); } } else { @@ -83,22 +50,22 @@ module.exports = () => { // 2. Add the codepush.gradle build task definitions var buildGradleContents = fs.readFileSync(buildGradlePath, "utf8"); var reactGradleLink = buildGradleContents.match(/\napply from: ["'].*?react\.gradle["']/)[0]; - var codePushGradleLink = `apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"`; + var codePushGradleLink = linkTools.codePushGradleLink; if (~buildGradleContents.indexOf(codePushGradleLink)) { console.log(`"codepush.gradle" is already linked in the build definition`); } else { buildGradleContents = buildGradleContents.replace(reactGradleLink, - `${reactGradleLink}\n${codePushGradleLink}`); + `${reactGradleLink}${codePushGradleLink}`); fs.writeFileSync(buildGradlePath, buildGradleContents); } //3. Add deployment key - var stringsResourcesPath = glob.sync("**/strings.xml", ignoreFolders)[0]; + var stringsResourcesPath = linkTools.getStringsResourcesPath(); if (!stringsResourcesPath) { return Promise.reject(new Error(`Couldn't find strings.xml. You might need to update it manually.`)); } else { var stringsResourcesContent = fs.readFileSync(stringsResourcesPath, "utf8"); - var deploymentKeyName = "reactNativeCodePush_androidDeploymentKey"; + var deploymentKeyName = linkTools.deploymentKeyName; if (~stringsResourcesContent.indexOf(deploymentKeyName)) { console.log(`${deploymentKeyName} already specified in the strings.xml`); } else { diff --git a/scripts/postlink/ios/postlink.js b/scripts/postlink/ios/postlink.js index 19681e4e0..948dbf419 100644 --- a/scripts/postlink/ios/postlink.js +++ b/scripts/postlink/ios/postlink.js @@ -1,9 +1,8 @@ + +var linkTools = require('../../tools/linkToolsIos'); var fs = require("fs"); -var glob = require("glob"); var inquirer = require('inquirer'); -var path = require("path"); var plist = require("plist"); -var xcode = require("xcode"); var semver = require('semver'); var package = require('../../../../../package.json'); @@ -12,15 +11,7 @@ module.exports = () => { console.log("Running ios postlink script"); - var ignoreNodeModules = { ignore: "node_modules/**" }; - var ignoreNodeModulesAndPods = { ignore: ["node_modules/**", "ios/Pods/**"] }; - var appDelegatePaths = glob.sync("**/AppDelegate.+(mm|m)", ignoreNodeModules); - - // Fix for https://github.com/Microsoft/react-native-code-push/issues/477 - // Typical location of AppDelegate.m for newer RN versions: $PROJECT_ROOT/ios//AppDelegate.m - // Let's try to find that path by filtering the whole array for any path containing - // If we can't find it there, play dumb and pray it is the first path we find. - var appDelegatePath = findFileByAppName(appDelegatePaths, package ? package.name : null) || appDelegatePaths[0]; + var appDelegatePath = linkTools.getAppDeletePath(); if (!appDelegatePath) { return Promise.reject(`Couldn't find AppDelegate. You might need to update it manually \ @@ -31,13 +22,12 @@ module.exports = () => { var appDelegateContents = fs.readFileSync(appDelegatePath, "utf8"); // 1. Add the header import statement - var codePushHeaderImportStatement = `#import `; - if (~appDelegateContents.indexOf(codePushHeaderImportStatement)) { + if (~appDelegateContents.indexOf(linkTools.codePushHeaderImportStatement)) { console.log(`"CodePush.h" header already imported.`); } else { var appDelegateHeaderImportStatement = `#import "AppDelegate.h"`; appDelegateContents = appDelegateContents.replace(appDelegateHeaderImportStatement, - `${appDelegateHeaderImportStatement}\n${codePushHeaderImportStatement}`); + `${appDelegateHeaderImportStatement}${linkTools.codePushHeaderImportStatementFormatted}`); } // 2. Modify jsCodeLocation value assignment @@ -46,8 +36,8 @@ module.exports = () => { if (!reactNativeVersion) { console.log(`Can't take react-native version from package.json`); } else if (semver.gte(semver.coerce(reactNativeVersion), "0.59.0")) { - var oldBundleUrl = "[[NSBundle mainBundle] URLForResource:@\"main\" withExtension:@\"jsbundle\"]"; - var codePushBundleUrl = "[CodePush bundleURL]"; + var oldBundleUrl = linkTools.oldBundleUrl; + var codePushBundleUrl = linkTools.codePushBundleUrl; if (~appDelegateContents.indexOf(codePushBundleUrl)) { console.log(`"BundleUrl" already pointing to "[CodePush bundleURL]".`); @@ -65,21 +55,16 @@ module.exports = () => { console.log('Couldn\'t find jsCodeLocation setting in AppDelegate.'); } - var newJsCodeLocationAssignmentStatement = "jsCodeLocation = [CodePush bundleURL];"; + var newJsCodeLocationAssignmentStatement = linkTools.codePushGradleLink; if (~appDelegateContents.indexOf(newJsCodeLocationAssignmentStatement)) { console.log(`"jsCodeLocation" already pointing to "[CodePush bundleURL]".`); } else { if (jsCodeLocations.length === 1) { - // If there is one `jsCodeLocation` it means that react-native app version is lower than 0.57.8 + // If there is one `jsCodeLocation` it means that react-native app version is not the 0.57.8 or 0.57.0 and lower than 0.59 // and we should replace this line with DEBUG ifdef statement and add CodePush call for Release case var oldJsCodeLocationAssignmentStatement = jsCodeLocations[0]; - var jsCodeLocationPatch = ` - #ifdef DEBUG - ${oldJsCodeLocationAssignmentStatement} - #else - ${newJsCodeLocationAssignmentStatement} - #endif`; + var jsCodeLocationPatch = linkTools.getJsCodeLocationPatch(oldJsCodeLocationAssignmentStatement); appDelegateContents = appDelegateContents.replace(oldJsCodeLocationAssignmentStatement, jsCodeLocationPatch); } else if (jsCodeLocations.length === 2) { @@ -94,7 +79,7 @@ module.exports = () => { } } - var plistPath = getPlistPath(); + var plistPath = linkTools.getPlistPath(); if (!plistPath) { return Promise.reject(`Couldn't find .plist file. You might need to update it manually \ @@ -128,103 +113,4 @@ module.exports = () => { fs.writeFileSync(appDelegatePath, appDelegateContents); fs.writeFileSync(plistPath, plistContents); } - - // Helper that filters an array with AppDelegate.m paths for a path with the app name inside it - // Should cover nearly all cases - function findFileByAppName(array, appName) { - if (array.length === 0 || !appName) return null; - - for (var i = 0; i < array.length; i++) { - var path = array[i]; - if (path && path.indexOf(appName) !== -1) { - return path; - } - } - - return null; - } - - function getDefaultPlistPath() { - //this is old logic in case we are unable to find PLIST from xcode/pbxproj - at least we can fallback to default solution - return glob.sync(`**/${package.name}/*Info.plist`, ignoreNodeModules)[0]; - } - - // This is enhanced version of standard implementation of xcode 'getBuildProperty' function - // but allows us to narrow results by PRODUCT_NAME property also. - // So we suppose that proj name should be the same as package name, otherwise fallback to default plist path searching logic - function getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, prop, targetProductName, build){ - var target; - var COMMENT_KEY = /_comment$/; - var PRODUCT_NAME_PROJECT_KEY = 'PRODUCT_NAME'; - var TV_OS_DEPLOYMENT_TARGET_PROPERTY_NAME = 'TVOS_DEPLOYMENT_TARGET'; - var TEST_HOST_PROPERTY_NAME = 'TEST_HOST'; - - var configs = parsedXCodeProj.pbxXCBuildConfigurationSection(); - for (var configName in configs) { - if (!COMMENT_KEY.test(configName)) { - var config = configs[configName]; - if ( (build && config.name === build) || (build === undefined) ) { - if (targetProductName) { - if (config.buildSettings[prop] !== undefined && config.buildSettings[PRODUCT_NAME_PROJECT_KEY] == targetProductName) { - target = config.buildSettings[prop]; - } - } else { - if (config.buildSettings[prop] !== undefined && - //exclude tvOS projects - config.buildSettings[TV_OS_DEPLOYMENT_TARGET_PROPERTY_NAME] == undefined && - //exclude test app - config.buildSettings[TEST_HOST_PROPERTY_NAME] == undefined) { - target = config.buildSettings[prop]; - } - } - } - } - } - return target; - } - - function getPlistPath(){ - var xcodeProjectPaths = glob.sync(`**/*.xcodeproj/project.pbxproj`, ignoreNodeModulesAndPods); - if (!xcodeProjectPaths){ - return getDefaultPlistPath(); - } - - if (xcodeProjectPaths.length !== 1) { - console.log('Could not determine correct xcode proj path to retrieve related plist file, there are multiple xcodeproj under the solution.'); - return getDefaultPlistPath(); - } - - var xcodeProjectPath = xcodeProjectPaths[0]; - var parsedXCodeProj; - - try { - var proj = xcode.project(xcodeProjectPath); - //use sync version because there are some problems with async version of xcode lib as of current version - parsedXCodeProj = proj.parseSync(); - } - catch(e) { - console.log('Couldn\'t read info.plist path from xcode project - error: ' + e.message); - return getDefaultPlistPath(); - } - - var INFO_PLIST_PROJECT_KEY = 'INFOPLIST_FILE'; - var RELEASE_BUILD_PROPERTY_NAME = "Release"; - var targetProductName = package ? package.name : null; - - //Try to get 'Release' build of ProductName matching the package name first and if it doesn't exist then try to get any other if existing - var plistPathValue = getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, targetProductName, RELEASE_BUILD_PROPERTY_NAME) || - getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, targetProductName) || - getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, null, RELEASE_BUILD_PROPERTY_NAME) || - getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY) || - parsedXCodeProj.getBuildProperty(INFO_PLIST_PROJECT_KEY, RELEASE_BUILD_PROPERTY_NAME) || - parsedXCodeProj.getBuildProperty(INFO_PLIST_PROJECT_KEY); - - if (!plistPathValue){ - return getDefaultPlistPath(); - } - - //also remove surrounding quotes from plistPathValue to get correct path resolved - //(see https://github.com/Microsoft/react-native-code-push/issues/534#issuecomment-302069326 for details) - return path.resolve(path.dirname(xcodeProjectPath), '..', plistPathValue.replace(/^"(.*)"$/, '$1')); - } -} \ No newline at end of file +} diff --git a/scripts/postunlink/android/postunlink.js b/scripts/postunlink/android/postunlink.js new file mode 100644 index 000000000..1c0a05796 --- /dev/null +++ b/scripts/postunlink/android/postunlink.js @@ -0,0 +1,74 @@ +var linkTools = require('../../tools/linkToolsAndroid'); +var fs = require("fs"); + +module.exports = () => { + + console.log("Running android postunlink script"); + + var mainApplicationPath = linkTools.getMainApplicationLocation(); + + // 1. Remove the getJSBundleFile override + var getJSBundleFileOverride = linkTools.getJSBundleFileOverride; + + if (mainApplicationPath) { + var mainApplicationContents = fs.readFileSync(mainApplicationPath, "utf8"); + if (!linkTools.isJsBundleOverridden(mainApplicationContents)) { + console.log(`"getJSBundleFile" is already removed`); + } else { + mainApplicationContents = mainApplicationContents.replace(`${getJSBundleFileOverride}`, ""); + fs.writeFileSync(mainApplicationPath, mainApplicationContents); + } + } else { + var mainActivityPath = linkTools.getMainActivityPath(); + if (mainActivityPath) { + var mainActivityContents = fs.readFileSync(mainActivityPath, "utf8"); + if (!linkTools.isJsBundleOverridden(mainActivityContents)) { + console.log(`"getJSBundleFile" is already removed`); + } else { + mainActivityContents = mainActivityContents.replace(getJSBundleFileOverride, ""); + fs.writeFileSync(mainActivityPath, mainActivityContents); + } + } else { + console.log(`Couldn't find Android application entry point. You might need to update it manually. \ + Please refer to plugin configuration section for Android at \ + https://github.com/microsoft/react-native-code-push#plugin-configuration-android for more details`); + } + } + + // 2. Remove the codepush.gradle build task definitions + var buildGradlePath = linkTools.getBuildGradlePath(); + + if (!fs.existsSync(buildGradlePath)) { + console.log(`Couldn't find build.gradle file. You might need to update it manually. \ + Please refer to plugin installation section for Android at \ + https://github.com/microsoft/react-native-code-push#plugin-installation-android---manual`); + } else { + var buildGradleContents = fs.readFileSync(buildGradlePath, "utf8"); + var codePushGradleLink = linkTools.codePushGradleLink; + if (!~buildGradleContents.indexOf(codePushGradleLink)) { + console.log(`"codepush.gradle" is already unlinked in the build definition`); + } else { + buildGradleContents = buildGradleContents.replace(`${codePushGradleLink}`,""); + fs.writeFileSync(buildGradlePath, buildGradleContents); + } + } + + // 3. Remove deployment key + var stringsResourcesPath = linkTools.getStringsResourcesPath(); + if (!stringsResourcesPath) { + return Promise.reject(new Error("Couldn't find strings.xml. You might need to update it manually.")); + } else { + var stringsResourcesContent = fs.readFileSync(stringsResourcesPath, "utf8"); + var deploymentKeyName = linkTools.deploymentKeyName; + if (!~stringsResourcesContent.indexOf(deploymentKeyName)) { + console.log(`${deploymentKeyName} already removed from the strings.xml`); + } else { + var AndroidDeploymentKey = stringsResourcesContent.match(/(.*<\/string>)/); + if (AndroidDeploymentKey) { + stringsResourcesContent = stringsResourcesContent.replace(`\n\t${AndroidDeploymentKey[0]}`,""); + fs.writeFileSync(stringsResourcesPath, stringsResourcesContent); + } + } + } + return Promise.resolve(); +} diff --git a/scripts/postunlink/ios/postunlink.js b/scripts/postunlink/ios/postunlink.js new file mode 100644 index 000000000..8631b72ea --- /dev/null +++ b/scripts/postunlink/ios/postunlink.js @@ -0,0 +1,87 @@ + +var linkTools = require('../../tools/linkToolsIos'); +var fs = require("fs"); +var plist = require("plist"); +var semver = require('semver'); +var packageFile = require('../../../../../package.json'); + +module.exports = () => { + + console.log("Running ios postunlink script"); + + var appDelegatePath = linkTools.getAppDeletePath(); + + if (!appDelegatePath) { + console.log(`Couldn't find AppDelegate. You might need to update it manually \ + Please refer to plugin configuration section for iOS at \ + https://github.com/microsoft/react-native-code-push#plugin-configuration-ios`); + } else { + var appDelegateContents = fs.readFileSync(appDelegatePath, "utf8"); + + // 1. Remove the header import statement + if (!~appDelegateContents.indexOf(linkTools.codePushHeaderImportStatement)) { + console.log(`"CodePush.h" header already removed.`); + } else { + appDelegateContents = appDelegateContents.replace(linkTools.codePushHeaderImportStatementFormatted, ""); + } + + // 2. Modify jsCodeLocation value assignment + var codePushBundleUrl = linkTools.codePushBundleUrl; + if (!~appDelegateContents.indexOf(codePushBundleUrl)) { + console.log(`"jsCodeLocation" already not pointing to "[CodePush bundleURL]".`); + } else { + var reactNativeVersion = packageFile && packageFile.dependencies && packageFile.dependencies["react-native"]; + if (!reactNativeVersion) { + console.log(`Can't take react-native version from package.json`); + } else if (semver.gte(semver.coerce(reactNativeVersion), "0.59.0")) { + var oldBundleUrl = linkTools.oldBundleUrl; + appDelegateContents = appDelegateContents.replace(codePushBundleUrl, oldBundleUrl); + fs.writeFileSync(appDelegatePath, appDelegateContents); + } else { + var linkedJsCodeLocationAssignmentStatement = linkTools.linkedJsCodeLocationAssignmentStatement; + var jsCodeLocations = appDelegateContents.match(/(jsCodeLocation = .*)/g); + if (!jsCodeLocations || jsCodeLocations.length !== 2 || !~appDelegateContents.indexOf(linkedJsCodeLocationAssignmentStatement)) { + console.log(`AppDelegate isn't compatible for unlinking`); + } else { + if (semver.eq(semver.coerce(reactNativeVersion), "0.57.8") || semver.eq(semver.coerce(reactNativeVersion), "0.57.0")) { + // If version of react-native application is 0.57.8 or 0.57 then by default there are two different + // jsCodeLocation for debug and release and we should replace only release + var unlinkedJsCodeLocations = `jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];`; + appDelegateContents = appDelegateContents.replace(linkedJsCodeLocationAssignmentStatement, + unlinkedJsCodeLocations); + } else { + // If version of react-native application is not 0.57.8 or 0.57 and lower than 0.59.0 then by default there is only one + // jsCodeLocation and we should stay on only it + var defaultJsCodeLocationAssignmentStatement = jsCodeLocations[0]; + var linkedCodeLocationPatch = linkTools.getJsCodeLocationPatch(defaultJsCodeLocationAssignmentStatement); + appDelegateContents = appDelegateContents.replace(linkedCodeLocationPatch, + defaultJsCodeLocationAssignmentStatement); + } + fs.writeFileSync(appDelegatePath, appDelegateContents); + } + } + } + } + + var plistPath = linkTools.getPlistPath(); + + if (!plistPath) { + return Promise.reject(`Couldn't find .plist file. You might need to update it manually \ + Please refer to plugin configuration section for iOS at \ + https://github.com/microsoft/react-native-code-push#plugin-configuration-ios`); + } + + var plistContents = fs.readFileSync(plistPath, "utf8"); + + // 3. Remove CodePushDeploymentKey from plist file + var parsedInfoPlist = plist.parse(plistContents); + if (!parsedInfoPlist.CodePushDeploymentKey) { + console.log(`"CodePushDeploymentKey" already removed from the plist file.`); + } else { + delete parsedInfoPlist.CodePushDeploymentKey; + plistContents = plist.build(parsedInfoPlist); + fs.writeFileSync(plistPath, plistContents); + } + + return Promise.resolve(); +} diff --git a/scripts/postunlink/run.js b/scripts/postunlink/run.js new file mode 100644 index 000000000..cf3789c7b --- /dev/null +++ b/scripts/postunlink/run.js @@ -0,0 +1,11 @@ +var postunlinks = [ + require("./ios/postunlink"), + require("./android/postunlink") +]; + +//run them sequentially +postunlinks + .reduce((p, fn) => p.then(fn), Promise.resolve()) + .catch((err) => { + console.error(err.message); + }); diff --git a/scripts/tools/linkToolsAndroid.js b/scripts/tools/linkToolsAndroid.js new file mode 100644 index 000000000..7da0d8f50 --- /dev/null +++ b/scripts/tools/linkToolsAndroid.js @@ -0,0 +1,57 @@ +var fs = require("fs"); +var glob = require("glob"); +var path = require("path"); + +var ignoreFolders = { ignore: ["node_modules/**", "**/build/**"] }; +var manifestPath = glob.sync("**/AndroidManifest.xml", ignoreFolders)[0]; + +exports.getJSBundleFileOverride = ` + @Override + protected String getJSBundleFile(){ + return CodePush.getJSBundleFile(); + } +`; +exports.reactNativeHostInstantiation = "new ReactNativeHost(this) {"; +exports.mainActivityClassDeclaration = "public class MainActivity extends ReactActivity {"; +exports.codePushGradleLink = `\napply from: "../../node_modules/react-native-code-push/android/codepush.gradle"`; +exports.deploymentKeyName = "reactNativeCodePush_androidDeploymentKey"; + +exports.getMainApplicationLocation = function () { + return findMainApplication() || glob.sync("**/MainApplication.java", ignoreFolders)[0]; +} + +exports.getMainActivityPath = function () { + return glob.sync("**/MainActivity.java", ignoreFolders)[0] +} + +exports.getStringsResourcesPath = function () { + return glob.sync("**/strings.xml", ignoreFolders)[0]; +} + +exports.getBuildGradlePath = function () { + return path.join("android", "app", "build.gradle"); +} + +exports.isJsBundleOverridden = function (codeContents) { + return /@Override\s*\n\s*protected String getJSBundleFile\(\)\s*\{[\s\S]*?\}/.test(codeContents); +} + +function findMainApplication() { + if (!manifestPath) { + return null; + } + + var manifest = fs.readFileSync(manifestPath, "utf8"); + + // Android manifest must include single 'application' element + var matchResult = manifest.match(/application\s+android:name\s*=\s*"(.*?)"/); + if (matchResult) { + var appName = matchResult[1]; + } else { + return null; + } + + var nameParts = appName.split('.'); + var searchPath = glob.sync("**/" + nameParts[nameParts.length - 1] + ".java", ignoreFolders)[0]; + return searchPath; +} diff --git a/scripts/tools/linkToolsIos.js b/scripts/tools/linkToolsIos.js new file mode 100644 index 000000000..4bca9f9dc --- /dev/null +++ b/scripts/tools/linkToolsIos.js @@ -0,0 +1,130 @@ +var glob = require("glob"); +var path = require("path"); +var xcode = require("xcode"); +var packageFile = require('../../../../package.json'); + +var ignoreNodeModules = { ignore: "node_modules/**" }; +var ignoreNodeModulesAndPods = { ignore: ["node_modules/**", "ios/Pods/**"] }; +var appDelegatePaths = glob.sync("**/AppDelegate.+(mm|m)", ignoreNodeModules); + +exports.codePushHeaderImportStatement = `#import `; +exports.codePushHeaderImportStatementFormatted = `\n${this.codePushHeaderImportStatement}`; +exports.codePushBundleUrl = "[CodePush bundleURL]"; +exports.oldBundleUrl = "[[NSBundle mainBundle] URLForResource:@\"main\" withExtension:@\"jsbundle\"]"; +exports.linkedJsCodeLocationAssignmentStatement = "jsCodeLocation = [CodePush bundleURL];"; + +exports.getJsCodeLocationPatch = function(defaultJsCodeLocationAssignmentStatement) { + return ` + #ifdef DEBUG + ${defaultJsCodeLocationAssignmentStatement} + #else + ${this.linkedJsCodeLocationAssignmentStatement} + #endif`; +} + +// Fix for https://github.com/Microsoft/react-native-code-push/issues/477 +// Typical location of AppDelegate.m for newer RN versions: $PROJECT_ROOT/ios//AppDelegate.m +// Let's try to find that path by filtering the whole array for any path containing +// If we can't find it there, play dumb and pray it is the first path we find. +exports.getAppDeletePath = function() { + return findFileByAppName(appDelegatePaths, packageFile ? packageFile.name : null) || appDelegatePaths[0]; +} + +exports.getPlistPath = function() { + var xcodeProjectPaths = glob.sync(`**/*.xcodeproj/project.pbxproj`, ignoreNodeModulesAndPods); + if (!xcodeProjectPaths){ + return getDefaultPlistPath(); + } + + if (xcodeProjectPaths.length !== 1) { + console.log('Could not determine correct xcode proj path to retrieve related plist file, there are multiple xcodeproj under the solution.'); + return getDefaultPlistPath(); + } + + var xcodeProjectPath = xcodeProjectPaths[0]; + var parsedXCodeProj; + + try { + var proj = xcode.project(xcodeProjectPath); + //use sync version because there are some problems with async version of xcode lib as of current version + parsedXCodeProj = proj.parseSync(); + } + catch(e) { + console.log('Couldn\'t read info.plist path from xcode project - error: ' + e.message); + return getDefaultPlistPath(); + } + + var INFO_PLIST_PROJECT_KEY = 'INFOPLIST_FILE'; + var RELEASE_BUILD_PROPERTY_NAME = "Release"; + var targetProductName = packageFile ? packageFile.name : null; + + //Try to get 'Release' build of ProductName matching the package name first and if it doesn't exist then try to get any other if existing + var plistPathValue = getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, targetProductName, RELEASE_BUILD_PROPERTY_NAME) || + getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, targetProductName) || + getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY, null, RELEASE_BUILD_PROPERTY_NAME) || + getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, INFO_PLIST_PROJECT_KEY) || + parsedXCodeProj.getBuildProperty(INFO_PLIST_PROJECT_KEY, RELEASE_BUILD_PROPERTY_NAME) || + parsedXCodeProj.getBuildProperty(INFO_PLIST_PROJECT_KEY); + + if (!plistPathValue){ + return getDefaultPlistPath(); + } + + //also remove surrounding quotes from plistPathValue to get correct path resolved + //(see https://github.com/Microsoft/react-native-code-push/issues/534#issuecomment-302069326 for details) + return path.resolve(path.dirname(xcodeProjectPath), '..', plistPathValue.replace(/^"(.*)"$/, '$1')); +} + +// Helper that filters an array with AppDelegate.m paths for a path with the app name inside it +// Should cover nearly all cases +function findFileByAppName(array, appName) { + if (array.length === 0 || !appName) return null; + + for (var i = 0; i < array.length; i++) { + var path = array[i]; + if (path && path.indexOf(appName) !== -1) { + return path; + } + } + + return null; +} + +function getDefaultPlistPath() { + //this is old logic in case we are unable to find PLIST from xcode/pbxproj - at least we can fallback to default solution + return glob.sync(`**/${packageFile.name}/*Info.plist`, ignoreNodeModules)[0]; +} + +// This is enhanced version of standard implementation of xcode 'getBuildProperty' function +// but allows us to narrow results by PRODUCT_NAME property also. +// So we suppose that proj name should be the same as package name, otherwise fallback to default plist path searching logic +function getBuildSettingsPropertyMatchingTargetProductName(parsedXCodeProj, prop, targetProductName, build) { + var target; + var COMMENT_KEY = /_comment$/; + var PRODUCT_NAME_PROJECT_KEY = 'PRODUCT_NAME'; + var TV_OS_DEPLOYMENT_TARGET_PROPERTY_NAME = 'TVOS_DEPLOYMENT_TARGET'; + var TEST_HOST_PROPERTY_NAME = 'TEST_HOST'; + + var configs = parsedXCodeProj.pbxXCBuildConfigurationSection(); + for (var configName in configs) { + if (!COMMENT_KEY.test(configName)) { + var config = configs[configName]; + if ( (build && config.name === build) || (build === undefined) ) { + if (targetProductName) { + if (config.buildSettings[prop] !== undefined && config.buildSettings[PRODUCT_NAME_PROJECT_KEY] == targetProductName) { + target = config.buildSettings[prop]; + } + } else { + if (config.buildSettings[prop] !== undefined && + //exclude tvOS projects + config.buildSettings[TV_OS_DEPLOYMENT_TARGET_PROPERTY_NAME] == undefined && + //exclude test app + config.buildSettings[TEST_HOST_PROPERTY_NAME] == undefined) { + target = config.buildSettings[prop]; + } + } + } + } + } + return target; +}