diff --git a/.gitignore b/.gitignore index 9e1210e74..1576fba0d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,9 @@ JsonStorage.json *.crt *.pem *.key -*.cnf \ No newline at end of file +*.cnf + +# Azurite local storage files +__azurite_db_* +__blobstorage__ +__queuestorage__ diff --git a/README.md b/README.md index cdbda97d6..484e3b18a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# Visual Studio App Center CodePush Standalone Version +# Archiving this repository + +Visual Studio App Center was retired on March 31, 2025, except for its Analytics and Diagnostics features. You can learn more about the retirement and the Analytics and Diagnostics extension [here](https://aka.ms/appcenter/retire). CodePush, along with other App Center features, was also retired on March 31, 2025. Consequently, we are archiving this repository. + +--- + +## Visual Studio App Center CodePush Standalone Version [CodePush](https://learn.microsoft.com/en-us/appcenter/distribution/codepush/) is an App Center feature that enables React Native developers to deploy mobile app updates directly to their users’ devices. It consists of two parts: CodePush Server where developers can publish app updates to (e.g. JS, HTML, CSS or image changes), and [CodePush React Native Client SDK](https://github.com/Microsoft/react-native-code-push) that enables querying for updates from within an app. diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 000000000..352ed8fb9 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,71 @@ +########################################## +# Environment Variables for CodePush Server +########################################## + +# ============================== +# Storage Configuration (REQUIRED - choose one) +# ============================== +EMULATED=false # Set to 'true' to use the local emulator + +# --- Azure Storage Configuration --- +AZURE_STORAGE_ACCOUNT= # Azure storage account name +AZURE_STORAGE_ACCESS_KEY= # Azure storage access key (if KeyVault not used) + +# ============================== +# Server Configuration (REQUIRED) +# ============================== +SERVER_URL=http://localhost:3000 # The URL of your server + +# ============================== +# Authentication (REQUIRED - at least one provider) +# ============================== + +# --- GitHub OAuth --- +GITHUB_CLIENT_ID= # GitHub OAuth client ID +GITHUB_CLIENT_SECRET= # GitHub OAuth client secret + +# --- Microsoft OAuth --- +MICROSOFT_CLIENT_ID= # Microsoft OAuth client ID +MICROSOFT_CLIENT_SECRET= # Microsoft OAuth client secret + +# ============================== +# Optional Configuration +# ============================== + +# --- HTTPS Configuration --- +HTTPS= # Set to 'true' to enable HTTPS for local deployment + +# --- Debugging Configuration --- +LOGGING=false # Enable CodePush-specific logging +DEBUG_DISABLE_AUTH=false # Disable OAuth authentication route +DEBUG_USER_ID= # Backend user ID for debugging session + +# ============================== +# Redis Configuration +# ============================== +REDIS_HOST= # Redis server IP address +REDIS_PORT=6379 # Redis port (default: 6379) +REDIS_KEY= # Redis authentication key + +# ============================== +# Unit Testing Configuration +# ============================== +TEST_AZURE_STORAGE=false # Run API unit tests against Azure storage +AZURE_ACQUISITION_URL= # URL for acquisition tests + +# ============================== +# Other Configuration +# ============================== +DISABLE_ACQUISITION=false # Disable acquisition routes +DISABLE_MANAGEMENT=false # Disable management routes +ENABLE_ACCOUNT_REGISTRATION=true # Enable account registration +UPLOAD_SIZE_LIMIT_MB=200 # Max file upload size (in MB) +ENABLE_PACKAGE_DIFFING=false # Enable generating diffs for releases + +# ============================== +# Azure KeyVault Configuration (Optional) +# ============================== +AZURE_KEYVAULT_ACCOUNT= # Azure KeyVault account name +CLIENT_ID= # Active Directory app client ID +CERTIFICATE_THUMBPRINT= # AD app certificate thumbprint +REFRESH_CREDENTIALS_INTERVAL=86400000 # Credential refresh interval (in ms, default: 1 day) diff --git a/api/ENVIRONMENT.md b/api/ENVIRONMENT.md index 0f4067e63..c7d0e0369 100644 --- a/api/ENVIRONMENT.md +++ b/api/ENVIRONMENT.md @@ -2,7 +2,7 @@ The CodePush Server is configured using environment variables. -Currently, the following environment variables are available. For convenience, we will also load the server environment from any '.env' file in the api directory, and the test environment from any '.test.env' file in the root directory. +For convenience, we will also load the server environment from any '.env' file in the api directory, and the test environment from any '.test.env' file in the root directory. Use the `.env.example` file as a template for setting up your environment variables. ## Mandatory parameters @@ -30,6 +30,7 @@ To emulate Azure Blob Storage locally. Azurite needs to be installed and running - `MICROSOFT_CLIENT_ID` - `MICROSOFT_CLIENT_SECRET` +- `MICROSOFT_TENANT_ID`: Required if application registration is single tenant. ## Optional parameters @@ -58,8 +59,8 @@ To perform the unit tests against Azure storage: To perform the unit tests against an Azure server: -- `AZURE_MANAGEMENT_URL`: (For unit tests) Set to a Azure url to run management tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. -- `AZURE_ACQUISITION_URL`: (For unit tests) Set to a Azure url to run acquisition tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. +- `AZURE_MANAGEMENT_URL`: (For unit tests) Set to an Azure url to run management tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. +- `AZURE_ACQUISITION_URL`: (For unit tests) Set to an Azure url to run acquisition tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. ### Other diff --git a/api/README.md b/api/README.md index b9747afd3..760bddf01 100644 --- a/api/README.md +++ b/api/README.md @@ -16,11 +16,26 @@ Additionally, you need to specify [EMULATED](ENVIRONMENT.md#emulated) flag equal #### Steps To run the CodePush Server locally, follow these steps: -1. Clone the CodePush Service repository. -1. Create a `.env` file and configure the mandatory variables as outlined in the `ENVIRONMENT.md` file. -1. Install dependencies by running `npm install`. -1. Build the server by running `npm run build`. -1. Start the server by running `npm run start:env`. + +1. Clone the CodePush Service repository to your local machine. + +2. Copy the `.env.example` file to a new file named `.env` in the root directory: + ````bash + cp .env.example .env + ```` + Fill in the values for each environment variable in the `.env` file according to your development or production setup. +3. Install all necessary dependencies: + ````bash + npm install + ```` +4. Compile the server code: + ````bash + npm run build + ```` +5. Launch the server with the environment-specific start command: + ````bash + npm run start:env + ```` By default, local CodePush server runs on HTTP. To run CodePush Server on HTTPS: @@ -102,7 +117,7 @@ Both work and personal accounts use the same application for authentication. The 1. Register an Azure Registered Application following [official guideline](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#register-an-application) 1. For option `Supported account types`: 1. If you want to support both Personal and Work accounts, select `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)` - 1. If you want to only support Work accounts, choose either `Accounts in this organizational directory only ( - Single tenant)` or `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)` depending if you want to support Single or Multitenant authorization + 1. If you want to only support Work accounts, choose either `Accounts in this organizational directory only ( - Single tenant)` or `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)` depending if you want to support Single or Multitenant authorization. Make sure to set `MICROSOFT_TENANT_ID` envrionment variable in case of using single tenant application. 1. If you want to only support Personal accounts, select `Personal Microsoft accounts only` 1. Set up Redirect URI(s) depending on the choice you made for `Supported account types`. If you choose both Personal and Work accounts, you need to add both redirect URIs, otherwise just one of the ones: 1. Personal account: `https://codepush-.azurewebsites.net/auth/callback/microsoft` (for local development it will be either http://localhost:3000/auth/callback/microsoft or https://localhost:8443/auth/callback/microsoft) @@ -115,3 +130,15 @@ Both work and personal accounts use the same application for authentication. The 1. Only letters are allowed. 1. Maximum 15 characters. + +## Metrics + +Installation metrics allow monitoring release activity via the CLI. For detailed usage instructions, please refer to the [CLI documentation](../cli/README.md#development-parameter). + +Redis is required for Metrics to work. + +### Steps + +1. Install Redis by following [official installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/). +1. TLS is required. Follow [official Redis TLS run guide](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/#running-manually). +1. Set the necessary environment variables for [Redis](./ENVIRONMENT.md#redis). \ No newline at end of file diff --git a/api/SECURITY.md b/api/SECURITY.md index 2ee12dc54..022413d51 100644 --- a/api/SECURITY.md +++ b/api/SECURITY.md @@ -19,4 +19,4 @@ All secrets used in the system should be handled with the utmost care. They must It is essential to review and apply security best practices for all system components. As this setup is minimal, it is the customer’s responsibility to harden the system for production use. - [Azure Storage Security Recommendations](https://learn.microsoft.com/en-us/azure/storage/blobs/security-recommendations) -- [Azure WebApp Security Best Practices](https://learn.microsoft.com/en-us/azure/app-service/overview-security) +- [Azure WebApp Security Best Practices](https://learn.microsoft.com/en-us/azure/app-service/overview-security) \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index 4575bed7e..45dfbb9c5 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,7 +18,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.19.2", + "express": "^4.21.2", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", @@ -2031,9 +2031,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -2671,16 +2671,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2694,7 +2695,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -2709,6 +2710,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-domain-middleware": { @@ -4077,9 +4082,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -4500,9 +4505,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/api/package.json b/api/package.json index 549e859e3..da3db1ada 100644 --- a/api/package.json +++ b/api/package.json @@ -32,7 +32,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.19.2", + "express": "^4.21.2", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", diff --git a/api/script/default-server.ts b/api/script/default-server.ts index 1c9234584..d6b0e6f0d 100644 --- a/api/script/default-server.ts +++ b/api/script/default-server.ts @@ -90,7 +90,7 @@ export function start(done: (err?: any, server?: express.Express, storage?: Stor next(); }); - if (process.env.LOGGING) { + if (process.env.LOGGING === "true") { app.use((req: express.Request, res: express.Response, next: (err?: any) => void): any => { console.log(); // Newline to mark new request console.log(`[REST] Received ${req.method} request at ${req.originalUrl}`); diff --git a/api/script/routes/passport-authentication.ts b/api/script/routes/passport-authentication.ts index 488d0d75a..38e0357ab 100644 --- a/api/script/routes/passport-authentication.ts +++ b/api/script/routes/passport-authentication.ts @@ -167,17 +167,17 @@ export class PassportAuthentication { router.get("/auth/login", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["hostname"] = req.query.hostname; - res.render("authenticate", { action: "login" }); + res.render("authenticate", { action: "login", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); router.get("/auth/link", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["authorization"] = req.query.access_token; - res.render("authenticate", { action: "link" }); + res.render("authenticate", { action: "link", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); router.get("/auth/register", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["hostname"] = req.query.hostname; - res.render("authenticate", { action: "register" }); + res.render("authenticate", { action: "register", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); return router; @@ -488,7 +488,9 @@ export class PassportAuthentication { redirectUrl: this.getCallbackUrl(providerName), clientID: microsoftClientId, clientSecret: microsoftClientSecret, - identityMetadata: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + identityMetadata: `https://login.microsoftonline.com/${ + process.env["MICROSOFT_TENANT_ID"] || "common" + }/v2.0/.well-known/openid-configuration`, responseMode: "query", responseType: "code", scope: ["email", "profile"], diff --git a/api/script/utils/hash-utils.ts b/api/script/utils/hash-utils.ts index abc66fd50..662e6ddbb 100644 --- a/api/script/utils/hash-utils.ts +++ b/api/script/utils/hash-utils.ts @@ -28,6 +28,8 @@ try { import Promise = q.Promise; const HASH_ALGORITHM = "sha256"; +const CODEPUSH_METADATA = '.codepushrelease'; + export function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise { if (!fs.lstatSync(directoryPath).isDirectory()) { throw new Error("Not a directory. Please either create a directory, or use hashFile()."); @@ -187,7 +189,14 @@ export class PackageManifest { public computePackageHash(): Promise { let entries: string[] = []; this._map.forEach((hash: string, name: string): void => { - entries.push(name + ":" + hash); + // .codepushrelease (relates to code signing feature) file + // should not be skipped in isIgnored() method. + // But to be equal with hashes computed in SDKs and CLI this file + // should be skipped when computing whole package hash + + if (name !== CODEPUSH_METADATA && !endsWith(name, '/' + CODEPUSH_METADATA)) { + entries.push(name + ':' + hash); + } }); // Make sure this list is alphabetically ordered so that other clients diff --git a/api/script/views/authenticate.ejs b/api/script/views/authenticate.ejs index cb6218986..33251b46f 100644 --- a/api/script/views/authenticate.ejs +++ b/api/script/views/authenticate.ejs @@ -85,11 +85,15 @@
Please select an authentication provider for your CodePush account:

- GitHub - - Microsoft (Personal) - - Microsoft (Work)
+ <% if (isGitHubAuthenticationEnabled) { %> + GitHub + + <% } %> + <% if (isMicrosoftAuthenticationEnabled) { %> + Microsoft (Personal) + + Microsoft (Work)
+ <% } %>
diff --git a/cli/README.md b/cli/README.md index b95bd6bab..1b82597ca 100644 --- a/cli/README.md +++ b/cli/README.md @@ -204,6 +204,12 @@ If having a staging and production version of your app is enough to meet your ne code-push-standalone deployment add ``` +If you want to re-use an existing deployment key, you can do this with: + +``` +code-push-standalone deployment add -k +``` + Just like with apps, you can remove and rename deployments as well, using the following commands respectively: ``` @@ -392,6 +398,13 @@ code-push-standalone release-react [--sourcemapOutput ] [--targetBinaryVersion ] [--rollout ] +[--useHermes ] +[--podFile ] +[--extraHermesFlags ] +[--privateKeyPath ] +[--xcodeProjectFile ] +[--xcodeTargetName ] +[--buildConfigurationName ] ``` The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: @@ -521,6 +534,48 @@ This specifies the relative path to where the assets, JS bundle and sourcemap fi _NOTE: This parameter can be set using either --outputDir or -o_ +#### Use Hermes parameter + +This parameter enforces the use of the Hermes compiler. If not specified, the automatic checks will be performed, inspecting the `build.gradle` and `Podfile` for the Hermes flag. + +_NOTE: This parameter can be set using either --hermesEnabled or -h_ + +#### Podfile parameter (iOS only) + +The Podfile path will be used for Hermes automatic check. Not used if `--useHermes` is specified. + +_NOTE: This parameter can be set using either --podfile or -pod_ + +#### Extra hermes flags parameter + +Hermes flags which will be passed to Hermes compiler. + +_NOTE: This parameter can be set using either --extraHermesFlags or -hf_ + +#### Private key path parameter + +Private key path which is used for code signing. + +_NOTE: This parameter can be set using either --privateKeyPath or -k_ + +#### Xcode project file parameter + +Path to the Xcode project or project.pbxproj file. + +_NOTE: This parameter can be set using either --xcodeProjectFile or -xp_ + +#### Xcode target name parameter + +Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only). + +_NOTE: This parameter can be set using either --xcodeTargetName or -xt_ + +#### Build configuration name parameter + +Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only). + +_NOTE: This parameter can be set using either --buildConfigurationName or -c_ + ## Debugging CodePush Integration Once you've released an update, React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: @@ -723,3 +778,60 @@ code-push-standalone deployment clear ``` After running this command, client devices configured to receive updates using its associated deployment key will no longer receive the updates that have been cleared. This command is irreversible, and therefore should not be used in a production deployment. + +## Code Signing for CodePush + +Code Signing ensures that updates deployed via CodePush are secure and verified. Follow these steps to set up Code Signing: + +### 1. Generate a Signing Key + +**Create private and public keys using OpenSSL:** + +```shell +# generate private RSA key and write it to private.pem file +openssl genrsa -out private.pem + +# export public key from private.pem into public.pem +openssl rsa -pubout -in private.pem -out public.pem +``` + +### 2. Configure CodePush CLI + +**Specify the path to your private key when releasing updates:** + +```shell +code-push-standalone release-react --privateKeyPath private.pem +``` + +### 3. Configure Your App + +#### iOS + +**Add the public key to your `Info.plist`:** + +- Open your `Info.plist` file. +- Add a new entry: + + ```xml + CodePushPublicKey + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... + -----END PUBLIC KEY----- + ``` + +Replace the placeholder with the actual contents of your `public.pem` file. + +#### Android + +**Add the public key to your `strings.xml`:** + +- Open `res/values/strings.xml`. +- Add the following entry: + + ```xml + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... + -----END PUBLIC KEY----- + ``` + +Replace the placeholder with the actual contents of your `public.pem` file. \ No newline at end of file diff --git a/cli/package-lock.json b/cli/package-lock.json index 2f8aa9bf1..7dcf49a0d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -13,9 +13,10 @@ "cli-table": "^0.3.11", "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "1.1.0", + "parse-duration": "^1.1.0", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", @@ -25,11 +26,12 @@ "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "simple-update-notifier": "^2.0.0", "slash": "1.0.0", "superagent": "^8.0.9", + "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", + "xcode": "^3.0.1", "xml2js": "^0.6.0", "yargs": "^17.7.2", "yazl": "^2.5.1" @@ -1049,6 +1051,14 @@ } ] }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1073,6 +1083,25 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1102,6 +1131,11 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1259,9 +1293,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -1424,6 +1458,14 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1736,9 +1778,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -1746,7 +1788,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2490,12 +2532,57 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2535,21 +2622,45 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/media-typer": { "version": "0.3.0", @@ -2641,6 +2752,14 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -2826,9 +2945,10 @@ } }, "node_modules/parse-duration": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", - "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.2.tgz", + "integrity": "sha512-p8EIONG8L0u7f8GFgfVlL4n8rnChTt8O5FSxgxMz2tjc9FMP199wxVKVB6IbKx11uTbKHACSvaLVIKNnoeNR/A==", + "license": "MIT" }, "node_modules/parseurl": { "version": "1.3.3", @@ -3208,7 +3328,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -3236,12 +3355,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -3388,15 +3504,14 @@ "tail": "^0.4.0" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" } }, "node_modules/sinon": { @@ -3454,6 +3569,14 @@ "node": ">= 0.8" } }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3584,6 +3707,41 @@ "node": ">= 0.4.0" } }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3709,6 +3867,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3783,6 +3949,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", @@ -3819,11 +3997,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/cli/package.json b/cli/package.json index e1ab92a2a..2c0acd2c7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -24,9 +24,10 @@ "cli-table": "^0.3.11", "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "1.1.0", + "parse-duration": "^1.1.0", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", @@ -36,11 +37,12 @@ "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "simple-update-notifier": "^2.0.0", "slash": "1.0.0", "superagent": "^8.0.9", + "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", + "xcode": "^3.0.1", "xml2js": "^0.6.0", "yargs": "^17.7.2", "yazl": "^2.5.1" diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 84377f90e..5e37358cc 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -21,6 +21,8 @@ const Table = require("cli-table"); const which = require("which"); import wordwrap = require("wordwrap"); import * as cli from "../script/types/cli"; +import sign from "./sign"; +const xcode = require("xcode"); import { AccessKey, Account, @@ -36,6 +38,17 @@ import { Session, UpdateMetrics, } from "../script/types"; +import { + getAndroidHermesEnabled, + getiOSHermesEnabled, + runHermesEmitBinaryCommand, + isValidVersion +} from "./react-native-utils"; +import { + fileDoesNotExistOrIsDirectory, + isBinaryOrZip, + fileExists +} from "./utils/file-utils"; const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); const emailValidator = require("email-validator"); @@ -277,7 +290,7 @@ function deleteFolder(folderPath: string): Promise { } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { - return sdk.addDeployment(command.appName, command.deploymentName).then((deployment: Deployment): void => { + return sdk.addDeployment(command.appName, command.deploymentName, command.key).then((deployment: Deployment): void => { log( 'Successfully added the "' + command.deploymentName + @@ -550,14 +563,6 @@ export function execute(command: cli.ICommand) { }); } -function fileDoesNotExistOrIsDirectory(filePath: string): boolean { - try { - return fs.lstatSync(filePath).isDirectory(); - } catch (error) { - return true; - } -} - function getTotalActiveFromDeploymentMetrics(metrics: DeploymentMetrics): number { let totalActive = 0; Object.keys(metrics).forEach((label: string) => { @@ -853,16 +858,6 @@ function getPackageMetricsString(obj: Package): string { } function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise { - const fileExists = (file: string): boolean => { - try { - return fs.statSync(file).isFile(); - } catch (e) { - return false; - } - }; - - const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version); - log(chalk.cyan(`Detecting ${command.platform} app version:\n`)); if (command.platform === "ios") { @@ -912,9 +907,13 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`); return Q(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).` - ); + 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).` + ); + } + + return getAppVersionFromXcodeProject(command, projectName); } } else { throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`); @@ -1050,6 +1049,53 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj } } +function getAppVersionFromXcodeProject(command: cli.IReleaseReactCommand, projectName: string): Promise { + const pbxprojFileName = "project.pbxproj"; + let resolvedPbxprojFile: string = command.xcodeProjectFile; + if (resolvedPbxprojFile) { + // If the xcode project 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(); + const marketingVersion = xcodeProj.getBuildProperty( + "MARKETING_VERSION", + command.buildConfigurationName, + command.xcodeTargetName + ); + 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).` + ); + } + console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`); + + return marketingVersion; +} + function printJson(object: any): void { log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); } @@ -1276,22 +1322,20 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = } } - if (command.appStoreVersion) { - throwForInvalidSemverRange(command.appStoreVersion); - } - const appVersionPromise: Promise = command.appStoreVersion ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); - if (command.outputDir) { - command.sourcemapOutput = path.join(command.outputDir, bundleName + ".map"); + if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) { + command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map"); } return appVersionPromise; }) .then((appVersion: string) => { + throwForInvalidSemverRange(appVersion); releaseCommand.appStoreVersion = appVersion; + return createEmptyTempReleaseFolder(outputFolder); }) // This is needed to clear the react native bundler cache: @@ -1307,6 +1351,31 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = command.sourcemapOutput ) ) + .then(async () => { + const isHermesEnabled = + command.useHermes || + (platform === "android" && (await getAndroidHermesEnabled(command.gradleFile))) || // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in build.gradle and we're releasing an Android build + (platform === "ios" && (await getiOSHermesEnabled(command.podFile))); // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build + + if (isHermesEnabled) { + log(chalk.cyan("\nRunning hermes compiler...\n")); + await runHermesEmitBinaryCommand( + bundleName, + outputFolder, + command.sourcemapOutput, + command.extraHermesFlags, + command.gradleFile + ); + } + }) + .then(async () => { + if (command.privateKeyPath) { + log(chalk.cyan("\nSigning the bundle:\n")); + await sign(command.privateKeyPath, outputFolder); + } else { + console.log("private key was not provided"); + } + }) .then(() => { log(chalk.cyan("\nReleasing update contents to CodePush:\n")); return release(releaseCommand); @@ -1474,10 +1543,6 @@ function releaseErrorHandler(error: CodePushError, command: cli.ICommand): void } } -function isBinaryOrZip(path: string): boolean { - return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1; -} - function throwForInvalidEmail(email: string): void { if (!emailValidator.validate(email)) { throw new Error('"' + email + '" is an invalid e-mail address.'); diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index aaa87172d..43b8b391e 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -4,7 +4,6 @@ import * as yargs from "yargs"; import * as cli from "../script/types/cli"; import * as chalk from "chalk"; -const updateNotifier = require("simple-update-notifier"); import backslash = require("backslash"); import parseDuration = require("parse-duration"); @@ -28,7 +27,6 @@ export function showHelp(showRootDescription?: boolean): void { console.log(chalk.cyan("======================================")); console.log(""); console.log("CodePush is a service that enables you to deploy mobile app updates directly to your users' devices.\n"); - updateCheck(); } yargs.showHelp(); @@ -36,13 +34,6 @@ export function showHelp(showRootDescription?: boolean): void { } } -function updateCheck(): void { - const notifier = updateNotifier({ pkg: packageJson }); - if (notifier.update) { - notifier.notify(); - } -} - function accessKeyAdd(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs @@ -331,6 +322,7 @@ yargs .command("list", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("list", yargs)) .command("ls", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("ls", yargs)) .command("transfer", "Transfer the ownership of an app to another account", (yargs: yargs.Argv) => { + isValidCommand = true; yargs .usage(USAGE_PREFIX + " app transfer ") .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments @@ -385,7 +377,14 @@ yargs yargs .usage(USAGE_PREFIX + " deployment add ") .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments - .example("deployment add MyApp MyDeployment", 'Adds deployment "MyDeployment" to app "MyApp"'); + .example("deployment add MyApp MyDeployment", 'Adds deployment "MyDeployment" to app "MyApp"') + .example("deployment add MyApp MyDeployment -k abc123", 'Adds deployment key "abc123"') + .option("key", { + alias: "k", + demand: false, + description: "Specify deployment key", + type: "string", + }); addCommonConfiguration(yargs); }) @@ -784,6 +783,56 @@ yargs "Path to where the bundle and sourcemap should be written. If omitted, a bundle and sourcemap will not be written.", type: "string", }) + .option("useHermes", { + alias: "h", + default: false, + demand: false, + description: "Enable hermes and bypass automatic checks", + type: "boolean", + }) + .option("podFile", { + alias: "pod", + default: null, + demand: false, + description: "Path to the cocopods config file (iOS only).", + type: "string", + }) + .option("extraHermesFlags", { + alias: "hf", + default: [], + demand: false, + description: + "Flags that get passed to Hermes, JavaScript to bytecode compiler. Can be specified multiple times.", + type: "array", + }) + .option("privateKeyPath", { + alias: "k", + default: null, + demand: false, + description: "Path to private key used for code signing.", + type: "string", + }) + .option("xcodeProjectFile", { + alias: "xp", + default: null, + demand: false, + description: "Path to the Xcode project or project.pbxproj file", + type: "string", + }) + .option("xcodeTargetName", { + alias: "xt", + default: undefined, + demand: false, + description: "Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only)", + type: "string", + }) + .option("buildConfigurationName", { + alias: "c", + default: undefined, + demand: false, + description: "Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only)", + type: "string", + }) .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); @@ -1004,6 +1053,10 @@ export function createCommand(): cli.ICommand { deploymentAddCommand.appName = arg2; deploymentAddCommand.deploymentName = arg3; + if(argv["key"]){ + deploymentAddCommand.key = argv["key"] as any; + } + } break; @@ -1178,6 +1231,13 @@ export function createCommand(): cli.ICommand { releaseReactCommand.rollout = getRolloutValue(argv["rollout"] as any); releaseReactCommand.sourcemapOutput = argv["sourcemapOutput"] as any; releaseReactCommand.outputDir = argv["outputDir"] as any; + releaseReactCommand.useHermes = argv["useHermes"] as any; + releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any; + releaseReactCommand.podFile = argv["podFile"] as any; + releaseReactCommand.privateKeyPath = argv["privateKeyPath"] as any; + releaseReactCommand.xcodeProjectFile = argv["xcodeProjectFile"] as any; + releaseReactCommand.xcodeTargetName = argv["xcodeTargetName"] as any; + releaseReactCommand.buildConfigurationName = argv["buildConfigurationName"] as any; } break; diff --git a/cli/script/management-sdk.ts b/cli/script/management-sdk.ts index 0d5af7a90..6b620a02e 100644 --- a/cli/script/management-sdk.ts +++ b/cli/script/management-sdk.ts @@ -253,8 +253,8 @@ class AccountManager { } // Deployments - public addDeployment(appName: string, deploymentName: string): Promise { - const deployment = { name: deploymentName }; + public addDeployment(appName: string, deploymentName: string, deploymentKey?: string): Promise { + const deployment = { name: deploymentName, key: deploymentKey }; return this.post(urlEncode([`/apps/${appName}/deployments/`]), JSON.stringify(deployment), /*expectResponseBody=*/ true).then( (res: JsonResponse) => res.body.deployment ); diff --git a/cli/script/react-native-utils.ts b/cli/script/react-native-utils.ts new file mode 100644 index 000000000..9270fed3c --- /dev/null +++ b/cli/script/react-native-utils.ts @@ -0,0 +1,283 @@ +import * as fs from "fs"; +import * as chalk from "chalk"; +import * as path from "path"; +import * as childProcess from "child_process"; +import { coerce, compare, valid } from "semver"; +import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils"; + +const g2js = require("gradle-to-js/lib/parser"); + +export function isValidVersion(version: string): boolean { + return !!valid(version) || /^\d+\.\d+$/.test(version); +} + +export async function runHermesEmitBinaryCommand( + bundleName: string, + outputFolder: string, + sourcemapOutput: string, + extraHermesFlags: string[], + gradleFile: string +): Promise { + const hermesArgs: string[] = []; + const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS; + + if (typeof envNodeArgs !== "undefined") { + Array.prototype.push.apply(hermesArgs, envNodeArgs.trim().split(/\s+/)); + } + + Array.prototype.push.apply(hermesArgs, [ + "-emit-binary", + "-out", + path.join(outputFolder, bundleName + ".hbc"), + path.join(outputFolder, bundleName), + ...extraHermesFlags, + ]); + + if (sourcemapOutput) { + hermesArgs.push("-output-source-map"); + } + + console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n")); + const hermesCommand = await getHermesCommand(gradleFile); + const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs); + console.log(`${hermesCommand} ${hermesArgs.join(" ")}`); + + return new Promise((resolve, reject) => { + hermesProcess.stdout.on("data", (data: Buffer) => { + console.log(data.toString().trim()); + }); + + hermesProcess.stderr.on("data", (data: Buffer) => { + console.error(data.toString().trim()); + }); + + hermesProcess.on("close", (exitCode: number, signal: string) => { + if (exitCode !== 0) { + reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`)); + } + // Copy HBC bundle to overwrite JS bundle + const source = path.join(outputFolder, bundleName + ".hbc"); + const destination = path.join(outputFolder, bundleName); + fs.copyFile(source, destination, (err) => { + if (err) { + console.error(err); + reject(new Error(`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`)); + } + fs.unlink(source, (err) => { + if (err) { + console.error(err); + reject(err); + } + resolve(null as void); + }); + }); + }); + }).then(() => { + if (!sourcemapOutput) { + // skip source map compose if source map is not enabled + return; + } + + const composeSourceMapsPath = getComposeSourceMapsPath(); + if (!composeSourceMapsPath) { + throw new Error("react-native compose-source-maps.js scripts is not found"); + } + + const jsCompilerSourceMapFile = path.join(outputFolder, bundleName + ".hbc" + ".map"); + if (!fs.existsSync(jsCompilerSourceMapFile)) { + throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`); + } + + return new Promise((resolve, reject) => { + const composeSourceMapsArgs = [composeSourceMapsPath, sourcemapOutput, jsCompilerSourceMapFile, "-o", sourcemapOutput]; + + // https://github.com/facebook/react-native/blob/master/react.gradle#L211 + // https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178 + // packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map + const composeSourceMapsProcess = childProcess.spawn("node", composeSourceMapsArgs); + console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(" ")}`); + + composeSourceMapsProcess.stdout.on("data", (data: Buffer) => { + console.log(data.toString().trim()); + }); + + composeSourceMapsProcess.stderr.on("data", (data: Buffer) => { + console.error(data.toString().trim()); + }); + + composeSourceMapsProcess.on("close", (exitCode: number, signal: string) => { + if (exitCode !== 0) { + reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`)); + } + + // Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well + fs.unlink(jsCompilerSourceMapFile, (err) => { + if (err) { + console.error(err); + reject(err); + } + + resolve(null); + }); + }); + }); + }); +} + +function parseBuildGradleFile(gradleFile: string) { + let buildGradlePath: string = path.join("android", "app"); + if (gradleFile) { + buildGradlePath = 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.`); + }); +} + +async function getHermesCommandFromGradle(gradleFile: string): Promise { + const buildGradle: any = await parseBuildGradleFile(gradleFile); + const hermesCommandProperty: any = Array.from(buildGradle["project.ext.react"] || []).find((prop: string) => + prop.trim().startsWith("hermesCommand:") + ); + if (hermesCommandProperty) { + return hermesCommandProperty.replace("hermesCommand:", "").trim().slice(1, -1); + } else { + return ""; + } +} + +export function getAndroidHermesEnabled(gradleFile: string): boolean { + return parseBuildGradleFile(gradleFile).then((buildGradle: any) => { + return Array.from(buildGradle["project.ext.react"] || []).some((line: string) => /^enableHermes\s{0,}:\s{0,}true/.test(line)); + }); +} + +export function getiOSHermesEnabled(podFile: string): boolean { + let podPath = path.join("ios", "Podfile"); + if (podFile) { + podPath = podFile; + } + if (fileDoesNotExistOrIsDirectory(podPath)) { + throw new Error(`Unable to find Podfile file "${podPath}".`); + } + + try { + const podFileContents = fs.readFileSync(podPath).toString(); + return /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents); + } catch (error) { + throw error; + } +} + +function getHermesOSBin(): string { + switch (process.platform) { + case "win32": + return "win64-bin"; + case "darwin": + return "osx-bin"; + case "freebsd": + case "linux": + case "sunos": + default: + return "linux64-bin"; + } +} + +function getHermesOSExe(): string { + const react63orAbove = compare(coerce(getReactNativeVersion()).version, "0.63.0") !== -1; + const hermesExecutableName = react63orAbove ? "hermesc" : "hermes"; + switch (process.platform) { + case "win32": + return hermesExecutableName + ".exe"; + default: + return hermesExecutableName; + } +} + +async function getHermesCommand(gradleFile: string): Promise { + const fileExists = (file: string): boolean => { + try { + return fs.statSync(file).isFile(); + } catch (e) { + return false; + } + }; + // Hermes is bundled with react-native since 0.69 + const bundledHermesEngine = path.join(getReactNativePackagePath(), "sdks", "hermesc", getHermesOSBin(), getHermesOSExe()); + if (fileExists(bundledHermesEngine)) { + return bundledHermesEngine; + } + + const gradleHermesCommand = await getHermesCommandFromGradle(gradleFile); + if (gradleHermesCommand) { + return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin())); + } else { + // assume if hermes-engine exists it should be used instead of hermesvm + const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe()); + if (fileExists(hermesEngine)) { + return hermesEngine; + } + return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes"); + } +} + +function getComposeSourceMapsPath(): string { + // detect if compose-source-maps.js script exists + const composeSourceMaps = path.join(getReactNativePackagePath(), "scripts", "compose-source-maps.js"); + if (fs.existsSync(composeSourceMaps)) { + return composeSourceMaps; + } + return null; +} + +function getReactNativePackagePath(): string { + const result = childProcess.spawnSync("node", ["--print", "require.resolve('react-native/package.json')"]); + const packagePath = path.dirname(result.stdout.toString()); + if (result.status === 0 && directoryExistsSync(packagePath)) { + return packagePath; + } + + return path.join("node_modules", "react-native"); +} + +export function directoryExistsSync(dirname: string): boolean { + try { + return fs.statSync(dirname).isDirectory(); + } catch (err) { + if (err.code !== "ENOENT") { + throw err; + } + } + return false; +} + +export function getReactNativeVersion(): string { + let packageJsonFilename; + let projectPackageJson; + try { + packageJsonFilename = path.join(process.cwd(), "package.json"); + projectPackageJson = JSON.parse(fs.readFileSync(packageJsonFilename, "utf-8")); + } 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.` + ); + } + + 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 && projectPackageJson.dependencies["react-native"]) || + (projectPackageJson.devDependencies && projectPackageJson.devDependencies["react-native"]) + ); +} \ No newline at end of file diff --git a/cli/script/sign.ts b/cli/script/sign.ts new file mode 100644 index 000000000..fd3b8fb59 --- /dev/null +++ b/cli/script/sign.ts @@ -0,0 +1,80 @@ +import * as fs from "fs/promises"; +import * as hashUtils from "./hash-utils"; +import * as path from "path"; +import * as jwt from "jsonwebtoken"; +import { copyFileToTmpDir, isDirectory } from "./utils/file-utils"; + +const CURRENT_CLAIM_VERSION: string = "1.0.0"; +const METADATA_FILE_NAME: string = ".codepushrelease"; + +interface CodeSigningClaims { + claimVersion: string; + contentHash: string; +} + +export default async function sign(privateKeyPath: string, updateContentsPath: string): Promise { + if (!privateKeyPath) { + return Promise.resolve(null); + } + + let privateKey: Buffer; + + try { + privateKey = await fs.readFile(privateKeyPath); + } catch (err) { + return Promise.reject(new Error(`The path specified for the signing key ("${privateKeyPath}") was not valid.`)); + } + + // If releasing a single file, copy the file to a temporary 'CodePush' directory in which to publish the release + try { + if (!isDirectory(updateContentsPath)) { + updateContentsPath = copyFileToTmpDir(updateContentsPath); + } + } catch (error) { + Promise.reject(error); + } + + const signatureFilePath: string = path.join(updateContentsPath, METADATA_FILE_NAME); + let prevSignatureExists = true; + try { + await fs.access(signatureFilePath, fs.constants.F_OK); + } catch (err) { + if (err.code === "ENOENT") { + prevSignatureExists = false; + } else { + return Promise.reject( + new Error( + `Could not delete previous release signature at ${signatureFilePath}. + Please, check your access rights.` + ) + ); + } + } + + if (prevSignatureExists) { + console.log(`Deleting previous release signature at ${signatureFilePath}`); + await fs.rmdir(signatureFilePath); + } + + const hash: string = await hashUtils.generatePackageHashFromDirectory(updateContentsPath, path.join(updateContentsPath, "..")); + const claims: CodeSigningClaims = { + claimVersion: CURRENT_CLAIM_VERSION, + contentHash: hash, + }; + + return new Promise((resolve, reject) => { + jwt.sign(claims, privateKey, { algorithm: "RS256" }, async (err: Error, signedJwt: string) => { + if (err) { + reject(new Error("The specified signing key file was not valid")); + } + + try { + await fs.writeFile(signatureFilePath, signedJwt); + console.log(`Generated a release signature and wrote it to ${signatureFilePath}`); + resolve(null); + } catch (error) { + reject(error); + } + }); + }); +} diff --git a/cli/script/types/cli.ts b/cli/script/types/cli.ts index cfd40596c..359ce1fc4 100644 --- a/cli/script/types/cli.ts +++ b/cli/script/types/cli.ts @@ -107,6 +107,7 @@ export interface IDebugCommand extends ICommand { export interface IDeploymentAddCommand extends ICommand { appName: string; deploymentName: string; + key?: string; default: boolean; } @@ -198,6 +199,12 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand { sourcemapOutput?: string; outputDir?: string; config?: string; + useHermes?: boolean; + extraHermesFlags?: string[]; + podFile?: string; + xcodeProjectFile?: string; + xcodeTargetName?: string; + buildConfigurationName?: string; } export interface IRollbackCommand extends ICommand { diff --git a/cli/script/utils/file-utils.ts b/cli/script/utils/file-utils.ts new file mode 100644 index 000000000..7881dd169 --- /dev/null +++ b/cli/script/utils/file-utils.ts @@ -0,0 +1,46 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as rimraf from "rimraf"; +import * as temp from "temp"; + +export function isBinaryOrZip(path: string): boolean { + return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1; +} + +export function isDirectory(path: string): boolean { + return fs.statSync(path).isDirectory(); +} + +export function fileExists(file: string): boolean { + try { + return fs.statSync(file).isFile(); + } catch (e) { + return false; + } +}; + +export function copyFileToTmpDir(filePath: string): string { + if (!isDirectory(filePath)) { + const outputFolderPath: string = temp.mkdirSync("code-push"); + rimraf.sync(outputFolderPath); + fs.mkdirSync(outputFolderPath); + + const outputFilePath: string = path.join(outputFolderPath, path.basename(filePath)); + fs.writeFileSync(outputFilePath, fs.readFileSync(filePath)); + + return outputFolderPath; + } +} + +export function fileDoesNotExistOrIsDirectory(path: string): boolean { + try { + return isDirectory(path); + } catch (error) { + return true; + } +} + +export function normalizePath(filePath: string): string { + //replace all backslashes coming from cli running on windows machines by slashes + return filePath.replace(/\\/g, "/"); +}