Compare commits

..
20 Commits
Author SHA1 Message Date
Wojciech Kozyra 20b091fbfa v0.22.2 2021-08-02 10:35:08 +02:00
Wojciech Kozyra 168b6afdfa [eas-cli] do not read BuildSettings on managed project 2021-08-02 10:10:03 +02:00
Dominik Sokal 3c6320b929 Update CHANGELOG.md 2021-07-30 14:03:18 +02:00
Dominik Sokal 4f1dc582bd [ENG-1570] evaluate interpolated iOS version strings (#541)
* evaluate interpolated ios version strings

* update CHANGELOG
2021-07-30 14:00:31 +02:00
Wojciech Kozyra daf2ef1e30 [eas-cli] fix unknown state error (#543)
* [eas-cli] fix unknown state error

* update CHANGELOG
2021-07-30 13:32:22 +02:00
Wojciech Kozyra f703bd8b5a [eas-cli] validate metadata in build request (#542)
* [eas-cli] validate metadata in build request

* Update CHANGELOG
2021-07-30 13:23:47 +02:00
Dominik Sokal 997cd7058a [ENG-1475] Additional filters for eas build:list (#540)
* generate graphql code

* use new App.builds api

* add more filters to build:list

* update CHANGELOG
2021-07-30 10:31:51 +02:00
Wojciech Kozyra e896c2f6c5 Update CHANGELOG 2021-07-29 14:55:04 +02:00
Wojciech Kozyra 3d723ca426 Use new eas.json format + auto migrate from old format (#537)
* Use new eas.json format + aot migrate old format

* review feedback

* review feedback 2

* fix
2021-07-29 14:50:08 +02:00
Dominik Sokal c17e1b3fa2 [ENG-1600] warn when building managed project that does not extend @expo/metro-config in metro.config.js (#534)
* warn when building managed project that does not extend @expo/metro-config in metro.config.js

* update CHANGELOG

* address PR feedback

* fix lint warnings/errors
2021-07-29 09:33:37 +02:00
Quinlan Jung d70fc36485 update changelog 2021-07-28 15:48:22 -07:00
Quinlan Jung 47784ca6d3 v0.22.1 2021-07-28 14:58:33 -07:00
Quinlan Jung 3e5a3aca27 [eas-cli] fix keystore upload bug (#538)
* [eas-cli] fix upload bug

* changelog
2021-07-28 14:55:36 -07:00
Dominik Sokal 89b0227594 [chore] upgrade: eslint, prettier, etc. (#536)
* upgrade: eslint, prettier, etc.

* run prettier
2021-07-28 11:12:06 +02:00
Dominik Sokal b98a30e7c6 install missing dep: fast-glob 2021-07-27 13:16:23 +02:00
Dominik Sokal 5c7862a685 upgrade typescript + clean up deps (#533)
* upgrade typescript to 4.3.5

* remove unused dependencies

* upgrade tslib

* update CHANGELOG
2021-07-27 12:22:39 +02:00
Bartłomiej KlocekandDominik Sokal 82833a9eb4 [eas submit][android] Autodetect google-services JSON key path (#520)
* foundation

* detection logic

* add tests

* Update CHANGELOG, ignore `node_modules`

* Display a chooser for multiple files

* Improve tests

* move `filterAsync` to a separate file

* Apply suggestions from code review

Co-authored-by: Dominik Sokal <[email protected]>

* Fix indent

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-27 12:22:25 +02:00
Dominik Sokal 66171c3c4b [ENG-1524] use envs from eas.json when resolving app config (#532)
* use envs from eas.json when resolving app config

* update CHANGELOG

* address PR feedback
2021-07-26 15:38:42 +02:00
Wojciech KozyraandDominik Sokal 179def91c7 [eas-cli] use .pbxproj file to check if native Xcode project is present (#531)
* [eas-cli] use .pbxproj file to check if native Xcode project is present

* Update CHANGELOG.md

* Update packages/eas-cli/src/project/ios/scheme.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-07-26 15:33:44 +02:00
Dominik Sokal 9f63ad4527 update CHANGELOG 2021-07-23 10:33:19 +02:00
89 changed files with 4679 additions and 2156 deletions

No files matched your search

+33 -2
View File
@@ -6,12 +6,45 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
### 🎉 New features
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
### 🎉 New features
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
@@ -19,8 +52,6 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.22.0"
"version": "0.22.2"
}
+4 -4
View File
@@ -17,12 +17,12 @@
"devDependencies": {
"@expo/oclif-dev-cli": "1.24.0-expo",
"@types/jest": "^26.0.13",
"eslint": "^6.6.0",
"eslint-config-universe": "^4.0.0",
"eslint-plugin-graphql": "^4.0.0",
"eslint": "7.31.0",
"eslint-config-universe": "7.0.1",
"eslint-plugin-graphql": "4.0.0",
"jest": "^26.4.2",
"jest-watch-typeahead": "^0.6.1",
"lerna": "^3.22.1",
"prettier": "^2.0.5"
"prettier": "2.3.2"
}
}
+49 -24
View File
@@ -42,6 +42,7 @@ eas --help COMMAND
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
@@ -71,7 +72,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -85,7 +86,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -99,7 +100,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -110,7 +111,7 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/analytics.ts)_
## `eas build`
@@ -130,7 +131,7 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -141,7 +142,7 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -155,7 +156,7 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:list`
@@ -166,12 +167,21 @@ USAGE
$ eas build:list
OPTIONS
--appBuildVersion=appBuildVersion
--appIdentifier=appIdentifier
--appVersion=appVersion
--buildProfile=buildProfile
--channel=channel
--distribution=(store|internal|simulator)
--gitCommitHash=gitCommitHash
--limit=limit
--platform=(all|android|ios)
--runtimeVersion=runtimeVersion
--sdkVersion=sdkVersion
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:view [BUILD_ID]`
@@ -182,7 +192,22 @@ USAGE
$ eas build:view [BUILD_ID]
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/build/view.ts)_
## `eas config`
Show the eas.json config
```
USAGE
$ eas config
OPTIONS
-p, --platform=(android|ios)
--profile=profile
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -193,7 +218,7 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -204,7 +229,7 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:list`
@@ -218,7 +243,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -229,7 +254,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -257,7 +282,7 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -271,7 +296,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -288,7 +313,7 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -305,7 +330,7 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -316,7 +341,7 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit --platform=(android|ios)`
@@ -396,7 +421,7 @@ EXAMPLES
and provide its App ID
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/submit.ts)_
## `eas webhook:create`
@@ -415,7 +440,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -429,7 +454,7 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -443,7 +468,7 @@ OPTIONS
--event=(BUILD) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -463,7 +488,7 @@ OPTIONS
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -477,5 +502,5 @@ ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.2/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+534 -229
View File
@@ -2771,9 +2771,19 @@
"name": "builds",
"description": "(EAS Build) Builds associated with this app",
"args": [
{
"name": "filter",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "BuildFilter",
"ofType": null
},
"defaultValue": null
},
{
"name": "offset",
"description": null,
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -2787,7 +2797,7 @@
},
{
"name": "limit",
"description": null,
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -2801,7 +2811,7 @@
},
{
"name": "status",
"description": null,
"description": "Deprecated, use filter instead",
"type": {
"kind": "ENUM",
"name": "BuildStatus",
@@ -2811,7 +2821,7 @@
},
{
"name": "platform",
"description": null,
"description": "Deprecated, use filter instead",
"type": {
"kind": "ENUM",
"name": "AppPlatform",
@@ -3019,6 +3029,69 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deployments",
"description": "Deployments associated with this app",
"args": [
{
"name": "limit",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "buildListMaxSize",
"description": "Max number of associated builds to return",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "mostRecentlyUpdatedAt",
"description": "Offset the query",
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Deployment",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iosAppCredentials",
"description": "iOS app credentials for the project",
@@ -3810,6 +3883,150 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "BuildFilter",
"description": "",
"fields": null,
"inputFields": [
{
"name": "platform",
"description": "",
"type": {
"kind": "ENUM",
"name": "AppPlatform",
"ofType": null
},
"defaultValue": null
},
{
"name": "status",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildStatus",
"ofType": null
},
"defaultValue": null
},
{
"name": "distribution",
"description": "",
"type": {
"kind": "ENUM",
"name": "DistributionType",
"ofType": null
},
"defaultValue": null
},
{
"name": "channel",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "appVersion",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "appBuildVersion",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sdkVersion",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "runtimeVersion",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "appIdentifier",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "buildProfile",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "gitCommitHash",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "AppPlatform",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "IOS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ANDROID",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildStatus",
@@ -3859,20 +4076,26 @@
},
{
"kind": "ENUM",
"name": "AppPlatform",
"name": "DistributionType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "IOS",
"name": "STORE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ANDROID",
"name": "INTERNAL",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SIMULATOR",
"description": "",
"isDeprecated": false,
"deprecationReason": null
@@ -3969,6 +4192,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cancelingActor",
"description": "",
"args": [],
"type": {
"kind": "INTERFACE",
"name": "Actor",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "artifacts",
"description": "",
@@ -5635,35 +5870,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "DistributionType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "STORE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INTERNAL",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SIMULATOR",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildIosEnterpriseProvisioning",
@@ -6776,6 +6982,298 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Deployment",
"description": "Represents a Deployment - a set of Builds with the same Runtime Version and Channel",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "channel",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "recentBuilds",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Build",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "branchToReceive",
"description": "",
"args": [],
"type": {
"kind": "OBJECT",
"name": "UpdateBranch",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateBranch",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updates",
"description": "",
"args": [
{
"name": "offset",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "limit",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "filter",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdatesFilter",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Update",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mostRecentUpdateForEachPlatform",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Update",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdatesFilter",
"description": "",
"fields": null,
"inputFields": [
{
"name": "platform",
"description": "",
"type": {
"kind": "ENUM",
"name": "AppPlatform",
"ofType": null
},
"defaultValue": null
},
{
"name": "runtimeVersions",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "IosAppCredentialsFilter",
@@ -9129,199 +9627,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateBranch",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appId",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updates",
"description": "",
"args": [
{
"name": "offset",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "limit",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "filter",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdatesFilter",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Update",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdatesFilter",
"description": "",
"fields": null,
"inputFields": [
{
"name": "platform",
"description": "",
"type": {
"kind": "ENUM",
"name": "AppPlatform",
"ofType": null
},
"defaultValue": null
},
{
"name": "runtimeVersions",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EnvironmentSecret",
+8 -12
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.22.0",
"version": "0.22.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -13,8 +13,8 @@
"@expo/apple-utils": "0.0.0-alpha.24",
"@expo/config": "3.3.42",
"@expo/config-plugins": "3.0.3",
"@expo/eas-build-job": "0.2.44",
"@expo/eas-json": "^0.22.0",
"@expo/eas-build-job": "0.2.45",
"@expo/eas-json": "^0.22.2",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
@@ -33,7 +33,7 @@
"cli-table3": "0.6.0",
"dateformat": "3.0.3",
"env-paths": "2.2.0",
"fast-glob": "3.2.4",
"fast-glob": "3.2.7",
"figures": "3.2.0",
"form-data": "3.0.0",
"fs-extra": "9.0.1",
@@ -64,7 +64,7 @@
"tempy": "0.7.0",
"terminal-link": "2.1.1",
"timeago.js": "4.0.2",
"tslib": "1.14.1",
"tslib": "2.3.0",
"turndown": "7.0.0",
"untildify": "4.0.0",
"uuid": "8.3.0",
@@ -79,7 +79,6 @@
"@graphql-codegen/introspection": "^1.18.0",
"@graphql-codegen/typescript": "^1.17.11",
"@graphql-codegen/typescript-operations": "^1.17.10",
"@types/color-string": "^1.5.0",
"@types/dateformat": "^3.0.1",
"@types/form-data": "^2.5.0",
"@types/fs-extra": "^9.0.1",
@@ -93,18 +92,15 @@
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.3",
"@types/tough-cookie": "^4.0.0",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "^8.3.0",
"@types/wrap-ansi": "^3.0.0",
"@vercel/ncc": "^0.23.0",
"axios": "^0.21.0",
"axios": "0.21.1",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"ts-node": "^8",
"typescript": "^3.9.7",
"wonka": "^4.0.14"
"typescript": "4.3.5"
},
"engines": {
"node": ">=8.0.0"
@@ -14,10 +14,11 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
const updatedBuildGradleContents =
AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
+22 -26
View File
@@ -1,5 +1,4 @@
import { Android, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -15,8 +14,12 @@ import {
} from '../../project/android/applicationId';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import {
BuildRequestSender,
CredentialsResult,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { Platform } from '../types';
import { logCredentialsSource } from '../utils/credentials';
@@ -25,15 +28,9 @@ import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = await createBuildContextAsync<Platform.ANDROID>({
commandCtx,
platform: Platform.ANDROID,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
Log.addNewLineIfNone();
@@ -51,18 +48,19 @@ This means that it will most likely produce an AAB and you will not be able to i
}
}
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
if (ctx.workflow === Workflow.GENERIC) {
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({}),
prepareJobAsync,
sendBuildRequestAsync: async (
appId: string,
@@ -90,21 +88,19 @@ async function ensureAndroidCredentialsAsync(
if (!shouldProvideCredentials(ctx)) {
return;
}
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.commandCtx.projectDir,
ctx.commandCtx.exp
);
const androidApplicationIdentifier = await getApplicationIdAsync(ctx.projectDir, ctx.exp);
const provider = new AndroidCredentialsProvider(
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
nonInteractive: ctx.commandCtx.nonInteractive,
await createCredentialsContextAsync(ctx.projectDir, {
exp: ctx.exp,
nonInteractive: ctx.nonInteractive,
}),
{
app: {
account: nullthrows(
findAccountByName(ctx.commandCtx.user.accounts, ctx.commandCtx.accountName),
`You do not have access to account: ${ctx.commandCtx.accountName}`
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.commandCtx.projectName,
projectName: ctx.projectName,
androidApplicationIdentifier,
},
}
@@ -1,4 +1,5 @@
import { Android, ArchiveSource, Job, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import path from 'path';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
@@ -13,13 +14,19 @@ interface JobData {
credentials?: AndroidCredentials;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -34,28 +41,28 @@ export async function prepareJobAsync(
}
: {};
const { gradleCommand } = ctx.buildProfile;
let buildType: Android.BuildType | undefined = ctx.buildProfile.buildType;
if (!buildType && !gradleCommand && ctx.buildProfile.distribution === 'internal') {
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const job = {
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
projectRootDirectory,
projectArchive: jobData.projectArchive,
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
image: buildProfile.image ?? 'default',
node: buildProfile.node,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
...buildCredentials,
@@ -63,11 +70,11 @@ export async function prepareJobAsync(
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
gradleCommand,
artifactPath: ctx.buildProfile.artifactPath,
gradleCommand: buildProfile.gradleCommand,
artifactPath: buildProfile.artifactPath,
username,
buildType,
buildType: buildProfile.developmentClient ? Android.BuildType.DEVELOPMENT_CLIENT : buildType,
};
return sanitizeJob(job);
+123 -24
View File
@@ -1,19 +1,24 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { UploadSessionType } from '../graphql/generated';
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { promptAsync } from '../prompts';
import { uploadAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { requestedPlatformDisplayNames } from './constants';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { collectMetadata } from './metadata';
import { MetadataContext, collectMetadata } from './metadata';
import { Platform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
import { printDeprecationWarnings } from './utils/printBuildInfo';
@@ -36,63 +41,62 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
getMetadataContext: () => MetadataContext<TPlatform>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
}
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends Job
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<() => Promise<string | undefined>> {
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
async () => await builder.ensureCredentialsAsync(builder.ctx),
async () => await builder.ensureCredentialsAsync(ctx),
{
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
}
);
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
if (!ctx.skipProjectConfiguration) {
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
trackingCtx: builder.ctx.trackingCtx,
trackingCtx: ctx.trackingCtx,
});
}
if (await vcs.hasUncommittedChangesAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${
requestedPlatformDisplayNames[builder.ctx.platform as Platform]
}`,
{
nonInteractive: builder.ctx.commandCtx.nonInteractive,
}
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const projectArchive = builder.ctx.commandCtx.local
const projectArchive = ctx.local
? ({
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync()).path,
} as const)
: ({
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(builder.ctx),
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadata = await collectMetadata(builder.ctx, {
credentialsSource: credentialsResult?.source,
});
const job = await builder.prepareJobAsync(builder.ctx, {
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadata(ctx, metadataContext);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
});
return async () => {
if (builder.ctx.commandCtx.local) {
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
@@ -177,7 +181,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata
): Promise<string> {
): Promise<BuildFragment> {
const { ctx } = builder;
return await withAnalyticsAsync(
async () => {
@@ -186,13 +190,13 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
}
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.commandCtx.projectId,
ctx.projectId,
job,
metadata
);
printDeprecationWarnings(deprecationInfo);
return build.id;
return build;
},
{
successEvent: Event.BUILD_REQUEST_SUCCESS,
@@ -274,3 +278,98 @@ async function reviewAndCommitChangesAsync(
});
}
}
export async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log(
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
);
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
+55 -72
View File
@@ -1,12 +1,13 @@
import { getConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import Log from '../log';
import Log, { learnMore } from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { promptAsync } from '../prompts';
import { confirmAsync, promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import vcs from '../vcs';
import { configureAndroidAsync } from './android/configure';
@@ -21,6 +22,37 @@ const configureCommitMessage = {
[RequestedPlatform.All]: 'Configure EAS Build',
};
export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
const message = 'This project is not configured to build with EAS. Set it up now?';
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
{ exit: 1 }
);
}
} else {
error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`,
{ exit: 1 }
);
}
}
export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
@@ -63,95 +95,46 @@ export async function configureAsync(options: {
}
}
const ANDROID_MANAGED_DEFAULTS = {
release: {
buildType: 'app-bundle',
},
const MANAGED_DEFAULTS = {
release: {},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const ANDROID_GENERIC_DEFAULTS = {
release: {
gradleCommand: ':app:bundleRelease',
},
development: {
gradleCommand: ':app:assembleDebug',
developmentClient: true,
distribution: 'internal',
},
};
const IOS_MANAGED_DEFAULTS = {
release: {
buildType: 'release',
},
const GENERIC_DEFAULTS = {
release: {},
development: {
buildType: 'development-client',
distribution: 'internal',
},
};
const IOS_GENERIC_DEFAULTS = {
release: {
schemeBuildConfiguration: 'Release',
},
development: {
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
},
};
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
let existingEasJson;
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
await reader.validateAsync();
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
existingEasJson = await reader.readRawAsync();
Log.withTick('Validated eas.json');
// If we have already populated eas.json with the default fields for the
// platform then proceed
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
return;
} else if (
ctx.requestedPlatform === 'all' &&
existingEasJson.builds.ios &&
existingEasJson.builds.android
) {
return;
}
return;
}
const shouldInitIOS =
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
const shouldInitAndroid =
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
const easJson = {
builds: {
...existingEasJson?.builds,
...(shouldInitAndroid
? {
android: ctx.hasAndroidNativeProject
? ANDROID_GENERIC_DEFAULTS
: ANDROID_MANAGED_DEFAULTS,
}
: null),
...(shouldInitIOS
? {
ios: ctx.hasIosNativeProject ? IOS_GENERIC_DEFAULTS : IOS_MANAGED_DEFAULTS,
}
: null),
},
};
const easJson =
ctx.hasAndroidNativeProject && ctx.hasIosNativeProject
? { build: GENERIC_DEFAULTS }
: { build: MANAGED_DEFAULTS };
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcs.trackFileAsync(easJsonPath);
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
Log.withTick('Generated eas.json');
}
enum ShouldCommitChanges {
+59 -93
View File
@@ -1,11 +1,12 @@
import { ExpoConfig } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, EasConfig, IosBuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { getProjectAccountName } from '../project/projectUtils';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { Actor } from '../user/User';
@@ -13,66 +14,6 @@ import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface CommandContext {
requestedPlatform: RequestedPlatform;
profile: string;
projectDir: string;
projectId: string;
user: Actor;
accountName: string;
projectName: string;
exp: ExpoConfig;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}
export async function createCommandContextAsync({
requestedPlatform,
profile,
exp,
projectDir,
projectId,
nonInteractive = false,
local,
clearCache = false,
skipProjectConfiguration = false,
waitForBuildEnd,
}: {
requestedPlatform: RequestedPlatform;
profile: string;
exp: ExpoConfig;
projectId: string;
projectDir: string;
nonInteractive: boolean;
local: boolean;
clearCache: boolean;
skipProjectConfiguration: boolean;
waitForBuildEnd: boolean;
}): Promise<CommandContext> {
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
return {
requestedPlatform,
profile,
projectDir,
projectId,
user,
accountName,
projectName,
exp,
nonInteractive,
local,
clearCache,
skipProjectConfiguration,
waitForBuildEnd,
};
}
export interface ConfigureContext {
user: Actor;
projectDir: string;
@@ -84,56 +25,83 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: IosBuildProfile;
export interface BuildContext<T extends Platform> {
commandCtx: CommandContext;
trackingCtx: TrackingContext;
accountName: string;
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
exp: ExpoConfig;
local: boolean;
nonInteractive: boolean;
platform: T;
buildProfile: PlatformBuildProfile<T>;
projectDir: string;
projectId: string;
projectName: string;
skipProjectConfiguration: boolean;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
easConfig,
commandCtx,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
easConfig: EasConfig;
commandCtx: CommandContext;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
if (!buildProfile) {
throw new Error(`${platform} build profile does not exist`);
}
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(commandCtx.projectDir, platform);
const accountId = findAccountByName(commandCtx.user.accounts, commandCtx.accountName)?.id;
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const devClientProperties = getDevClientEventProperties({
platform,
projectDir: commandCtx.projectDir,
projectDir,
buildProfile,
workflow,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: commandCtx.accountName,
project_id: commandCtx.projectId,
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
return {
commandCtx,
workflow,
trackingCtx,
platform,
accountName,
buildProfile,
buildProfileName,
clearCache,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
}
@@ -141,21 +109,19 @@ function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
workflow,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
workflow: Workflow;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'schemeBuildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.schemeBuildConfiguration === 'Debug');
} else if ('buildType' in buildProfile) {
includesDevClient = buildProfile.buildType === 'development-client';
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} else {
includesDevClient = false;
}
-173
View File
@@ -1,173 +0,0 @@
import { EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import ora from 'ora';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { sleep } from '../utils/promise';
import vcs from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { CommandContext } from './context';
import { prepareIosBuildAsync } from './ios/build';
import { Platform, RequestedPlatform } from './types';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
if (commandCtx.local) {
return;
}
Log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
Log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(scheduledBuilds.map(i => i.buildId));
printBuildResults(commandCtx.accountName, builds);
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
async function startBuildsAsync(
commandCtx: CommandContext
): Promise<{ platform: Platform; buildId: string }[]> {
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const shouldBuildIos = [RequestedPlatform.Ios, RequestedPlatform.All].includes(
commandCtx.requestedPlatform
);
const easConfig = await new EasJsonReader(
commandCtx.projectDir,
commandCtx.requestedPlatform
).readAsync(commandCtx.profile);
const builds: {
platform: Platform;
sendBuildRequestAsync: () => Promise<string | undefined>;
}[] = [];
if (shouldBuildAndroid) {
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
}
if (shouldBuildIos) {
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
}
return (
await Promise.all(
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
platform,
buildId: await sendBuildRequestAsync(),
}))
)
).filter((build): build is { platform: Platform; buildId: string } => !!build.buildId);
}
async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 3600, intervalSec = 30 } = {}
): Promise<(BuildFragment | null)[]> {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
while (time <= endTime) {
const builds: (BuildFragment | null)[] = await Promise.all(
buildIds.map(async buildId => {
try {
return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) {
return null;
}
})
);
if (builds.length === 1) {
const build = nullthrows(builds[0]);
switch (build.status) {
case BuildStatus.Finished:
spinner.succeed('Build finished');
return builds;
case BuildStatus.New:
spinner.text = 'Build created';
break;
case BuildStatus.InQueue:
spinner.text = 'Build queued...';
break;
case BuildStatus.Canceled:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.InProgress:
spinner.text = 'Build in progress...';
break;
case BuildStatus.Errored:
spinner.fail('Build failed');
if (build.error) {
return builds;
} else {
throw new Error(`Standalone build failed!`);
}
default:
spinner.warn('Unknown status.');
throw new Error(`Unknown status: ${builds} - aborting!`);
}
} else {
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
spinner.succeed('All builds have finished');
return builds;
} else if (
builds.filter(build =>
build?.status
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
]
.filter(i => i)
.join('\t');
}
}
time = new Date().getTime();
await sleep(intervalSec * 1000);
}
spinner.warn('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
process.exit(1);
}
}
@@ -149,7 +149,7 @@ describe(readBuildNumberAsync, () => {
describe('generic project', () => {
it('reads the build number from native code', async () => {
const exp = initGenericProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -157,7 +157,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/repo', exp);
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -167,21 +167,33 @@ describe(readShortVersionAsync, () => {
describe('generic project', () => {
it('reads the short version from native code', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initGenericProject({
shortVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync('/repo', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
});
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initGenericProject();
const shortVersion = await readShortVersionAsync('/repo', exp);
const shortVersion = await readShortVersionAsync('/repo', exp, {});
expect(shortVersion).toBe('1.0.0');
});
});
});
function initGenericProject(): ExpoConfig {
function initGenericProject({
shortVersion = '1.0.0',
version = '1',
}: { shortVersion?: string; version?: string } = {}): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
@@ -197,9 +209,9 @@ function initGenericProject(): ExpoConfig {
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>${shortVersion}</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>${version}</string>
</dict>
</plist>`,
},
+31 -26
View File
@@ -1,14 +1,15 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
import { EasConfig } from '@expo/eas-json';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContextAsync } from '../context';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { IosMetadataContext } from '../metadata';
import { Platform } from '../types';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
@@ -16,51 +17,55 @@ import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareIosBuildAsync(
commandCtx: CommandContext,
easConfig: EasConfig
): Promise<() => Promise<string | undefined>> {
const buildCtx = await createBuildContextAsync<Platform.IOS>({
commandCtx,
platform: Platform.IOS,
easConfig,
});
const { buildProfile } = buildCtx;
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
const { buildProfile } = ctx;
if (buildCtx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(
commandCtx.projectDir,
commandCtx.exp
);
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: commandCtx.projectDir,
nonInteractive: commandCtx.nonInteractive,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
});
},
getMetadataContext: (): IosMetadataContext => {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
}
const applicationTarget = findApplicationTarget(targets);
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
return { buildSettings };
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
+1 -1
View File
@@ -38,7 +38,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
+7 -11
View File
@@ -16,20 +16,16 @@ export async function ensureIosCredentialsAsync(
return;
}
const { projectDir } = buildCtx.commandCtx;
const ctx = await createCredentialsContextAsync(projectDir, {
nonInteractive: buildCtx.commandCtx.nonInteractive,
const credentialsCtx = await createCredentialsContextAsync(buildCtx.projectDir, {
exp: buildCtx.exp,
nonInteractive: buildCtx.nonInteractive,
});
const provider = new IosCredentialsProvider(ctx, {
app: getAppFromContext(ctx),
const provider = new IosCredentialsProvider(credentialsCtx, {
app: getAppFromContext(credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.commandCtx.projectDir,
buildCtx.workflow
),
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -45,5 +41,5 @@ export async function ensureIosCredentialsAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
return !buildCtx.buildProfile.simulator;
}
+18 -11
View File
@@ -15,13 +15,18 @@ interface JobData {
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const projectRootDirectory =
path.relative(await vcs.getRootPathAsync(), ctx.commandCtx.projectDir) || '.';
const username = getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync());
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -30,12 +35,12 @@ export async function prepareJobAsync(
}
}
const job = {
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
distribution: ctx.buildProfile.distribution,
distribution: ctx.buildProfile.scheme ? 'simulator' : ctx.buildProfile.distribution,
builderEnvironment: {
image: resolveImage(ctx),
node: ctx.buildProfile.node,
@@ -47,8 +52,9 @@ export async function prepareJobAsync(
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.commandCtx.clearCache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
@@ -57,20 +63,21 @@ export async function prepareJobAsync(
updates: { channel: ctx.buildProfile.channel },
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.schemeBuildConfiguration,
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
buildType: ctx.buildProfile.buildType,
buildType: ctx.buildProfile.developmentClient
? Ios.BuildType.DEVELOPMENT_CLIENT
: Ios.BuildType.RELEASE,
username,
};
return sanitizeJob(job);
}
function resolveImage(ctx: BuildContext<Platform.IOS>): Ios.BuilderEnvironment['image'] {
// see https://linear.app/expo/issue/ENG-1396/make-default-image-dependent-on-sdk-version
if (!ctx.buildProfile.image && ctx.commandCtx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.commandCtx.exp.sdkVersion);
if (!ctx.buildProfile.image && ctx.exp.sdkVersion) {
const majorSdkVersion = semver.major(ctx.exp.sdkVersion);
if (majorSdkVersion <= 41) {
return 'macos-catalina-10.15-xcode-12.4';
}
+11 -4
View File
@@ -4,11 +4,13 @@ import { Platform, Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { evaluateString } from '../utils/template';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
@@ -104,12 +106,16 @@ export async function bumpVersionInAppJsonAsync({
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleShortVersionString;
return (
infoPlist.CFBundleShortVersionString &&
evaluateString(infoPlist.CFBundleShortVersionString, buildSettings)
);
} else {
return exp.version;
}
@@ -117,12 +123,13 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir);
return infoPlist.CFBundleVersion;
return infoPlist.CFBundleVersion && evaluateString(infoPlist.CFBundleVersion, buildSettings);
} else {
return IOSConfig.Version.getBuildNumber(exp);
}
+38 -28
View File
@@ -1,5 +1,6 @@
import { Metadata } from '@expo/eas-build-job';
import { CredentialsSource, IosEnterpriseProvisioning } from '@expo/eas-json';
import { Metadata, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
@@ -27,56 +28,65 @@ import { isExpoUpdatesInstalled } from './utils/updates';
*/
const packageJSON = require('../../package.json');
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? object
: IosMetadataContext;
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function collectMetadata<T extends Platform>(
ctx: BuildContext<T>,
{
credentialsSource,
}: {
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
}
platformContext?: MetadataContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
return {
const metadata = {
trackingContext: ctx.trackingCtx,
appVersion: await resolveAppVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx),
appBuildVersion: await resolveAppBuildVersionAsync(ctx, platformContext),
cliVersion: packageJSON.version,
workflow: ctx.workflow,
credentialsSource,
sdkVersion: ctx.commandCtx.exp.sdkVersion,
runtimeVersion: ctx.commandCtx.exp.runtimeVersion,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
distribution: ctx.buildProfile.distribution ?? 'store',
appName: ctx.commandCtx.exp.name,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx),
buildProfile: ctx.commandCtx.profile,
buildProfile: ctx.buildProfileName,
gitCommitHash: await vcs.getCommitHashAsync(),
username: getUsername(ctx.commandCtx.exp, await ensureLoggedInAsync()),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
),
}),
};
return sanitizeMetadata(metadata);
}
async function resolveAppVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readShortVersionAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
return await readVersionNameAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await readVersionNameAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveAppBuildVersionAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: BuildContext<T>,
platformContext?: MetadataContext<T>
): Promise<string | undefined> {
if (ctx.platform === Platform.IOS) {
return await readBuildNumberAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
const versionCode = await readVersionCodeAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
return versionCode !== undefined ? String(versionCode) : undefined;
}
}
@@ -85,16 +95,16 @@ async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
if (ctx.platform === Platform.IOS) {
return await getBundleIdentifierAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
} else {
return await getApplicationIdAsync(ctx.commandCtx.projectDir, ctx.commandCtx.exp);
return await getApplicationIdAsync(ctx.projectDir, ctx.exp);
}
}
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
@@ -116,10 +126,10 @@ async function getNativeReleaseChannelAsync<T extends Platform>(
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? 'default';
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
@@ -131,10 +141,10 @@ async function getNativeChannelAsync<T extends Platform>(
): Promise<string | undefined> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
case Platform.IOS: {
return (await readIosChannelSafelyAsync(ctx.commandCtx.projectDir)) ?? undefined;
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
}
}
+6
View File
@@ -15,4 +15,10 @@ export enum BuildStatus {
CANCELED = 'canceled',
}
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
SIMULATOR = 'simulator',
}
export type TrackingContext = Record<string, string | number | boolean>;
@@ -11,7 +11,6 @@ import { getBuildLogsUrl } from './url';
export function formatGraphQLBuild(build: BuildFragment) {
const actor = getActorName(build);
const account = build.project.__typename === 'App' ? build.project.ownerAccount.name : 'unknown';
const fields: { label: string; value?: string | null }[] = [
{ label: 'ID', value: build.id },
{
@@ -77,7 +76,7 @@ export function formatGraphQLBuild(build: BuildFragment) {
},
{
label: 'Logs',
value: getBuildLogsUrl({ buildId: build.id, account }),
value: getBuildLogsUrl(build),
},
{
label: 'Artifact',
@@ -12,51 +12,34 @@ import {
EasBuildDeprecationInfoType,
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../constants';
import { appPlatformDisplayNames, appPlatformEmojis } from '../constants';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(
accountName: string,
builds: { platform: 'android' | 'ios'; buildId: string }[]
): void {
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
const { buildId } = builds[0];
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(({ buildId, platform }) => {
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${requestedPlatformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`
`${appPlatformDisplayNames[build.platform]} build details: ${chalk.underline(logsUrl)}`
);
});
}
}
export function printBuildResults(accountName: string, builds: (BuildFragment | null)[]): void {
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(accountName, build);
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build =>
printBuildResult(accountName, build)
);
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
function printBuildResult(accountName: string, build: BuildFragment): void {
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
const userError = build.error;
@@ -80,10 +63,7 @@ function printBuildResult(accountName: string, build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
const logsUrl = getBuildLogsUrl({
buildId: build.id,
account: accountName,
});
const logsUrl = getBuildLogsUrl(build);
const installUrl = getInternalDistributionInstallUrl(build);
qrcodeTerminal.generate(installUrl, code => Log.log(`${indentString(code, 2)}\n`));
Log.log(
@@ -0,0 +1,5 @@
import template from 'lodash/template';
export function evaluateString(s: string, vars: Record<string, any>): string {
return template(s, { interpolate: /\$\(([\s\S]+?)\)/g })(vars);
}
+9 -8
View File
@@ -3,14 +3,15 @@ import assert from 'assert';
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
export function getBuildLogsUrl({
buildId,
account,
}: {
buildId: string;
account: string;
}): string {
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build;
if (project.__typename === 'App') {
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/projects/${
project.name
}/builds/${build.id}`;
} else {
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
}
}
export function getArtifactUrl(artifactId: string): string {
@@ -67,9 +67,9 @@ async function selectBuildToCancelAsync(
let builds;
try {
const [newBuilds, inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.New }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.New } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InQueue } }),
BuildQuery.allForAppAsync(projectId, { filter: { status: BuildStatus.InProgress } }),
]);
spinner.stop();
builds = [...newBuilds, ...inQueueBuilds, ...inProgressBuilds];
+197 -145
View File
@@ -1,24 +1,54 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { configureAsync } from '../../build/configure';
import { createCommandContextAsync } from '../../build/context';
import { buildAsync } from '../../build/create';
import { RequestedPlatform } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { Workflow } from '@expo/eas-build-job';
import {
EasJsonReader,
hasMismatchedExtendsAsync,
isUsingDeprecatedFormatAsync,
migrateAsync,
} from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error, exit } from '@oclif/errors';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { Platform, RequestedPlatform } from '../../build/types';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { BuildFragment, BuildStatus } from '../../graphql/generated';
import Log from '../../log';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts';
import vcs from '../../vcs';
interface RawBuildFlags {
platform?: string;
'skip-credentials-check': boolean;
'skip-project-configuration': boolean;
profile: string;
'non-interactive': boolean;
local: boolean;
wait: boolean;
'clear-cache': boolean;
}
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
clearCache: boolean;
}
export default class Build extends EasCommand {
static description = 'Start a build';
@@ -56,155 +86,177 @@ export default class Build extends EasCommand {
};
async run(): Promise<void> {
const { flags } = this.parse(Build);
const { flags: rawFlags } = this.parse(Build);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const { requestedPlatform } = flags;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await vcs.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
const startedBuilds: BuildFragment[] = [];
let metroConfigValidated = false;
for (const platform of platformsToBuild) {
const ctx = await createBuildContextAsync({
buildProfileName: flags.profile,
clearCache: flags.clearCache,
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
projectDir,
skipProjectConfiguration: flags.skipProjectConfiguration,
});
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
await validateMetroConfigForManagedWorkflowAsync(ctx);
metroConfigValidated = true;
}
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const maybeBuild = await this.startBuildAsync(ctx);
if (maybeBuild) {
startedBuilds.push(maybeBuild);
}
}
if (flags.local) {
return;
}
Log.newLine();
printLogsUrls(startedBuilds);
Log.newLine();
if (flags.wait) {
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
printBuildResults(builds);
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
}
}
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
const nonInteractive = flags['non-interactive'];
if (!flags.platform && nonInteractive) {
throw new Error('--platform is required when building in non-interactive mode');
}
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const requestedPlatform =
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
if (flags.local) {
if (requestedPlatform === RequestedPlatform.All) {
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
}
}
if (flags['skip-credentials-check']) {
Log.warnDeprecatedFlag(
'skip-credentials-check',
'Build credential validation is always skipped with the --non-interactive flag. You can also skip interactively.'
'Build credentials validation is always skipped with the --non-interactive flag. You can also skip interactively.'
);
Log.newLine();
}
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
let { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!flags.local && !(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (flags.local && !verifyOptionsForLocalBuilds(platform)) {
process.exitCode = 1;
return;
}
exp = (await ensureProjectConfiguredAsync(projectDir, platform)) ?? exp;
const commandCtx = await createCommandContextAsync({
requestedPlatform: platform,
profile: flags.profile,
exp,
projectDir,
projectId,
nonInteractive,
clearCache: flags['clear-cache'],
local: flags.local,
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
waitForBuildEnd: flags.wait,
profile: flags['profile'],
nonInteractive,
local: flags['local'],
wait: flags['wait'],
clearCache: flags['clear-cache'],
};
}
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{ title: 'All', value: RequestedPlatform.All },
{ title: 'iOS', value: RequestedPlatform.Ios },
{ title: 'Android', value: RequestedPlatform.Android },
],
});
await buildAsync(commandCtx);
}
}
function verifyOptionsForLocalBuilds(platform: RequestedPlatform): boolean {
if (platform === RequestedPlatform.All) {
Log.error('Builds for multiple platforms are not supported with flag --local');
return false;
} else if (process.platform !== 'darwin' && platform === RequestedPlatform.Ios) {
Log.error('Unsupported platform, macOS is required to build apps for iOS');
return false;
} else {
return true;
}
}
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message: 'Build for platforms',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
async function ensureProjectConfiguredAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<ExpoConfig | null> {
const platformsToConfigure = await getPlatformsToConfigureAsync(projectDir, platform);
if (!platformsToConfigure) {
return null;
}
// Ensure the prompt is consistent with the platforms we need to configure
let message = 'This project is not configured to build with EAS. Set it up now?';
if (platformsToConfigure === RequestedPlatform.Ios) {
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
} else if (platformsToConfigure === RequestedPlatform.Android) {
message = 'Your Android project is not configured to build with EAS. Set it up now?';
}
const confirm = await confirmAsync({ message });
if (confirm) {
await configureAsync({
projectDir,
platform: platformsToConfigure,
});
if (await vcs.hasUncommittedChangesAsync()) {
throw new Error(
'Build process requires clean working tree, please commit all your changes and run `eas build` again'
);
}
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
return exp;
} else {
throw new Error(
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
'https://docs.expo.io/build/eas-json'
)})`
);
}
}
async function getPlatformsToConfigureAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<RequestedPlatform | null> {
if (!(await fs.pathExists(path.join(projectDir, 'eas.json')))) {
return platform;
}
const easConfig = await new EasJsonReader(projectDir, platform).readRawAsync();
if (platform === RequestedPlatform.All) {
if (easConfig.builds?.android && easConfig.builds?.ios) {
return null;
} else if (easConfig.builds?.ios) {
return RequestedPlatform.Android;
} else if (easConfig.builds?.android) {
return RequestedPlatform.Ios;
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
if (requestedPlatform === RequestedPlatform.All) {
return [Platform.ANDROID, Platform.IOS];
} else if (requestedPlatform === RequestedPlatform.Android) {
return [Platform.ANDROID];
} else {
return [Platform.IOS];
}
} else if (
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
easConfig.builds?.[platform]
) {
return null;
}
return platform;
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
let sendBuildRequestAsync: BuildRequestSender;
if (ctx.platform === Platform.ANDROID) {
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
} else {
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
}
return await sendBuildRequestAsync();
}
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
i => i.status === BuildStatus.Errored
);
if (failedBuilds.length > 0) {
exit(1);
}
}
}
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (await isUsingDeprecatedFormatAsync(projectDir)) {
const hasMismatchedExtendsKeys = await hasMismatchedExtendsAsync(projectDir);
if (nonInteractive) {
Log.error('We detected that your eas.json is using a deprecated format.');
Log.error(
'We will convert it automatically if run this command without --non-interactive flag. Alternatively, you can update eas.json manually according to https://docs.expo.io/build/eas-json'
);
error('Unsupported eas.json format', { exit: 1 });
}
const confirm = await confirmAsync({
message:
'We detected that your eas.json is using a deprecated format, do you want to migrate to the new format?',
});
if (confirm) {
await migrateAsync(projectDir);
if (hasMismatchedExtendsKeys) {
Log.warn(
'"extends" keyword can only be migrated automatically to the new format if both Android and iOS profiles extend base profiles with the same names for both platforms'
);
Log.warn(
'Migration was successful, but you need to manually adjust the extend rules for your profiles'
);
error('Fix eas.json manually', { exit: 1 });
}
} else {
Log.error(
"Aborting, update your eas.json according to https://docs.expo.io/build/eas-json and run 'eas build' again"
);
error('Unsupported eas.json format', { exit: 1 });
}
}
}
+57 -8
View File
@@ -3,9 +3,13 @@ import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import ora from 'ora';
import { BuildStatus, RequestedPlatform } from '../../build/types';
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import { AppPlatform, BuildStatus as GraphQLBuildStatus } from '../../graphql/generated';
import {
AppPlatform,
DistributionType,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
@@ -31,16 +35,36 @@ export default class BuildList extends Command {
BuildStatus.CANCELED,
],
}),
distribution: flags.enum({
options: [
BuildDistributionType.STORE,
BuildDistributionType.INTERNAL,
BuildDistributionType.SIMULATOR,
],
}),
channel: flags.string(),
appVersion: flags.string(),
appBuildVersion: flags.string(),
sdkVersion: flags.string(),
runtimeVersion: flags.string(),
appIdentifier: flags.string(),
buildProfile: flags.string(),
gitCommitHash: flags.string(),
limit: flags.integer(),
};
async run() {
const { platform: requestedPlatform, status: buildStatus, limit = 10 } = this.parse(
BuildList
).flags;
const { flags } = this.parse(BuildList);
const {
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = flags;
const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
@@ -52,8 +76,19 @@ export default class BuildList extends Command {
try {
const builds = await BuildQuery.allForAppAsync(projectId, {
limit,
platform,
status: graphqlBuildStatus,
filter: {
platform,
status: graphqlBuildStatus,
distribution: graphqlBuildDistribution,
channel: flags.channel,
appVersion: flags.appVersion,
appBuildVersion: flags.appBuildVersion,
sdkVersion: flags.sdkVersion,
runtimeVersion: flags.runtimeVersion,
appIdentifier: flags.appIdentifier,
buildProfile: flags.buildProfile,
gitCommitHash: flags.gitCommitHash,
},
});
if (builds.length) {
@@ -90,7 +125,7 @@ const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | und
}
};
const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | undefined => {
const toGraphQLBuildStatus = (buildStatus?: BuildStatus): GraphQLBuildStatus | undefined => {
if (!buildStatus) {
return undefined;
} else if (buildStatus === BuildStatus.NEW) {
@@ -107,3 +142,17 @@ const toGraphQLBuildStatus = (buildStatus: BuildStatus): GraphQLBuildStatus | un
return GraphQLBuildStatus.Canceled;
}
};
const toGraphQLBuildDistribution = (
buildDistribution?: BuildDistributionType
): DistributionType | undefined => {
if (buildDistribution === BuildDistributionType.STORE) {
return DistributionType.Store;
} else if (buildDistribution === BuildDistributionType.INTERNAL) {
return DistributionType.Internal;
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
return DistributionType.Simulator;
} else {
return undefined;
}
};
@@ -37,9 +37,7 @@ async function promptForRolloutPercentAsync({
return rolloutPercent;
}
function getRolloutInfo(
getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery
): {
function getRolloutInfo(getUpdateChannelByNameForAppResult: GetChannelByNameForAppQuery): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
@@ -48,12 +46,14 @@ function getRolloutInfo(
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.branchMapping
);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const newBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === newBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!newBranch || !oldBranch) {
throw new Error(
@@ -140,9 +140,10 @@ async function startRolloutAsync({
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch = getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
const oldBranch =
getUpdateChannelByNameForAppResult.app?.byId.updateChannelByName?.updateBranches.filter(
branch => branch.id === oldBranchId
)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${fullName}"`
@@ -37,9 +37,11 @@ export type BranchMapping = {
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(
branchMappingString?: string
): { branchMapping: BranchMapping; isRollout: boolean; rolloutPercent?: number } {
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
+68
View File
@@ -0,0 +1,68 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { Command, flags } from '@oclif/command';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
import { selectAsync } from '../prompts';
import { handleDeprecatedEasJsonAsync } from './build';
export { Platform } from '@expo/eas-build-job';
export default class Config extends Command {
static description = 'Show the eas.json config';
static flags = {
platform: flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: flags.string(),
};
async run() {
const { flags } = this.parse(Config);
const { platform: maybePlatform, profile: maybeProfile } = flags as {
platform?: Platform;
profile?: string;
};
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await handleDeprecatedEasJsonAsync(projectDir, false);
const reader = new EasJsonReader(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
).map(profileName => ({
title: profileName,
value: profileName,
}))
));
const platform =
maybePlatform ??
(await selectAsync('Select platform', [
{
title: 'Android',
value: Platform.ANDROID,
},
{
title: 'iOS',
value: Platform.IOS,
},
]));
const profile = await reader.readBuildProfileAsync(profileName, platform);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
Log.newLine();
Log.log(JSON.stringify(config, null, 2));
Log.newLine();
Log.newLine();
Log.log(`Build profile "${profileName}" from eas.json for platform ${platform}`);
Log.newLine();
Log.log(JSON.stringify(profile, null, 2));
}
}
@@ -13,9 +13,8 @@ export class AssignFcm {
ctx: Context,
fcm: AndroidFcmFragment
): Promise<CommonAndroidAppCredentialsFragment> {
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
this.app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(this.app);
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync(
appCredentials,
{
@@ -23,9 +23,8 @@ export async function canCopyLegacyCredentialsAsync(
return false; // modern credentials already exist
}
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
return !!legacyAppCredentials; // user has some legacy credentials
}
@@ -41,16 +40,14 @@ export async function promptUserAndCopyLegacyCredentialsAsync(
const spinner = ora('Classic credentials detected, copying to EAS...').start();
try {
const legacyAppCredentials = await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(
app
);
const legacyAppCredentials =
await ctx.android.getLegacyAndroidAppCredentialsWithCommonFieldsAsync(app);
if (!legacyAppCredentials) {
return;
}
const appCredentials = await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
app
);
const appCredentials =
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(app);
const legacyFcm = legacyAppCredentials.androidFcm;
if (legacyFcm) {
const clonedFcm = await ctx.android.createFcmAsync(
@@ -120,9 +117,8 @@ export async function createOrUpdateDefaultAndroidAppBuildCredentialsAsync(
!ctx.nonInteractive,
'createOrUpdateDefaultAndroidAppBuildCredentialsAsync must be run in interactive mode'
);
const existingDefaultBuildCredentials = await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(
appLookupParams
);
const existingDefaultBuildCredentials =
await ctx.android.getDefaultAndroidAppBuildCredentialsAsync(appLookupParams);
if (existingDefaultBuildCredentials) {
return await ctx.android.updateAndroidAppBuildCredentialsAsync(
existingDefaultBuildCredentials,
@@ -25,6 +25,7 @@ export class CreateKeystore {
if (providedKeystore) {
const providedKeystoreWithType = getKeystoreWithType(providedKeystore);
validateKeystore(providedKeystoreWithType);
return providedKeystoreWithType;
}
return await generateRandomKeystoreAsync();
}
@@ -1,11 +1,15 @@
import { confirmAsync } from '../../../../prompts';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateKeystore } from '../CreateKeystore';
jest.mock('../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
jest.mock('../../../utils/promptForCredentials');
describe('CreateKeystore', () => {
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -16,6 +20,20 @@ describe('CreateKeystore', () => {
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
});
it('creates a keystore by uploading', async () => {
(askForUserProvidedAsync as jest.Mock).mockImplementation(() => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
await createKeystoreAction.runAsync(ctx);
// expect keystore to be created on expo servers
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledWith(
appLookupParams.account,
expect.objectContaining(testKeystore)
);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
@@ -120,9 +120,8 @@ export async function createAndroidAppBuildCredentialsAsync(
androidKeystoreId: string;
}
): Promise<AndroidAppBuildCredentialsFragment> {
const androidAppCredentials = await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(
appLookupParams
);
const androidAppCredentials =
await createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync(appLookupParams);
const buildCredentialsList = androidAppCredentials.androidAppBuildCredentialsList;
const existingDefaultBuildCredentials =
buildCredentialsList.find(buildCredentials => buildCredentials.isDefault) ?? null;
+9 -6
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import pick from 'lodash/pick';
import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName } from '../project/projectUtils';
import { confirmAsync } from '../prompts';
import { Actor, getActorDisplayName } from '../user/User';
@@ -23,6 +24,7 @@ interface AppleCtxOptions {
interface Options extends AppleCtxOptions {
nonInteractive?: boolean;
exp?: ExpoConfig;
}
export interface Context {
@@ -45,12 +47,13 @@ export async function createCredentialsContextAsync(
): Promise<Context> {
const user = await ensureLoggedInAsync();
let expoConfig: ExpoConfig | undefined;
try {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
expoConfig = exp;
} catch (error) {
// ignore error, context might be created outside of expo project
let expoConfig: ExpoConfig | undefined = options.exp;
if (!expoConfig) {
try {
expoConfig = getExpoConfig(projectDir);
} catch (error) {
// ignore error, context might be created outside of expo project
}
}
return new CredentialsContext(projectDir, user, expoConfig, options);
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CredentialsSource, DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
@@ -18,7 +18,7 @@ import { isAdHocProfile } from './utils/provisioningProfile';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -67,12 +67,8 @@ export class ConfigureProvisioningProfile {
app: AppLookupParams,
profileFromApple: ProvisioningProfileStoreInfo
): Promise<AppleProvisioningProfileMutationResult> {
const {
developerPortalIdentifier,
certificateP12,
certificatePassword,
serialNumber,
} = this.distributionCertificate;
const { developerPortalIdentifier, certificateP12, certificatePassword, serialNumber } =
this.distributionCertificate;
assert(
certificateP12 && certificatePassword,
'Distribution Certificate P12 and Password is required'
@@ -101,11 +101,12 @@ export class SetupAdhocProvisioningProfile {
const chosenDevices = await chooseDevices(registeredAppleDevices, provisionedDeviceIdentifiers);
// 4. Reuse or create the profile on Apple Developer Portal
const provisioningProfileStoreInfo = await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
const provisioningProfileStoreInfo =
await ctx.appStore.createOrReuseAdhocProvisioningProfileAsync(
chosenDevices.map(({ identifier }) => identifier),
this.app.bundleIdentifier,
distCert.serialNumber
);
// 5. Create or update the profile on servers
const appleAppIdentifier = await ctx.ios.createOrGetExistingAppleAppIdentifierAsync(
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -12,7 +12,7 @@ import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
interface Options {
app: App;
targets: Target[];
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -1,4 +1,4 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
distribution: IosDistributionType;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
@@ -52,11 +52,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
@@ -80,11 +81,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -112,11 +114,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
@@ -145,11 +148,12 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction = new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
);
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
@@ -25,24 +25,30 @@ const validateBooleanOptions = (options: any): boolean => {
return typeof options === 'boolean';
};
const validatePrefixedStringArrayOptions = (prefix: string) => (options: any): boolean => {
return (
Array.isArray(options) &&
options.every(option => typeof option === 'string' && option.startsWith(prefix))
);
};
const validatePrefixedStringArrayOptions =
(prefix: string) =>
(options: any): boolean => {
return (
Array.isArray(options) &&
options.every(option => typeof option === 'string' && option.startsWith(prefix))
);
};
const validateStringArrayOptions = (options: any): boolean => {
return Array.isArray(options) && options.every(option => typeof option === 'string');
};
const createValidateStringOptions = (allowed: string[]) => (options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringOptions =
(allowed: string[]) =>
(options: any): boolean => {
return allowed.includes(options);
};
const createValidateStringArrayOptions = (allowed: string[]) => (options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const createValidateStringArrayOptions =
(allowed: string[]) =>
(options: any): boolean => {
return Array.isArray(options) && options.every(option => allowed.includes(option));
};
const validateDevProdString = createValidateStringOptions(['development', 'production']);
@@ -73,9 +73,8 @@ async function findProfileByBundleIdAsync(
// there is an expo managed profile with our desired certificate
// return the profile that will be valid for the longest duration
return {
profile: expoProfilesWithCertificate.sort(sortByExpiration)[
expoProfilesWithCertificate.length - 1
],
profile:
expoProfilesWithCertificate.sort(sortByExpiration)[expoProfilesWithCertificate.length - 1],
didUpdate: false,
};
} else if (expoProfiles) {
@@ -228,16 +227,12 @@ export async function createOrReuseAdhocProvisioningProfileAsync(
const spinner = ora(`Handling Apple ad hoc provisioning profiles`).start();
try {
const context = getRequestContext(authCtx);
const {
didUpdate,
didCreate,
profileName,
...adhocProvisioningProfile
} = await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
const { didUpdate, didCreate, profileName, ...adhocProvisioningProfile } =
await manageAdHocProfilesAsync(context, {
udids,
bundleId: bundleIdentifier,
certSerialNumber: distCertSerialNumber,
});
if (didCreate) {
spinner.succeed(`Created new profile: ${profileName}`);
@@ -11,6 +11,7 @@ export interface App {
export interface Target {
targetName: string;
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
}
@@ -21,12 +21,10 @@ describe('print credentials', () => {
},
projectName: 'test52',
};
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
'@quinlanj/test52',
{
const testIosAppCredentialsData =
await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync('@quinlanj/test52', {
appleAppIdentifierId: 'test-id',
}
);
});
const appCredentials = {
test52: nullthrows(testIosAppCredentialsData),
};
@@ -162,14 +162,8 @@ function displayIosAppBuildCredentials(buildCredentials: IosAppBuildCredentialsF
const maybeProvProf = buildCredentials.provisioningProfile;
Log.log(` Provisioning Profile:`);
if (maybeProvProf) {
const {
expiration,
updatedAt,
status,
developerPortalIdentifier,
appleTeam,
appleDevices,
} = maybeProvProf;
const { expiration, updatedAt, status, developerPortalIdentifier, appleTeam, appleDevices } =
maybeProvProf;
if (developerPortalIdentifier) {
Log.log(` Developer Portal ID: ${developerPortalIdentifier}`);
}
@@ -1,4 +1,5 @@
import { EasJsonReader, IosBuildProfile } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { IosBuildProfile } from '@expo/eas-json';
import assert from 'assert';
import nullthrows from 'nullthrows';
@@ -201,9 +202,8 @@ export class ManageIos {
const iosAppCredentialsMap: IosAppCredentialsMap = {};
for (const target of targets) {
const appLookupParams = getAppLookupParamsFromContext(ctx, target);
iosAppCredentialsMap[
target.targetName
] = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
iosAppCredentialsMap[target.targetName] =
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
}
displayIosCredentials(app, iosAppCredentialsMap, targets);
}
@@ -285,9 +285,10 @@ export class ManageIos {
}
const app = { account, projectName: ctx.exp.slug };
const easJsonReader = new EasJsonReader(ctx.projectDir, 'ios');
const easConfig = await new SelectBuildProfileFromEasJson(easJsonReader).runAsync(ctx);
const buildProfile = nullthrows(easConfig.builds.ios, 'iOS build profile must be defined');
const buildProfile = await new SelectBuildProfileFromEasJson(
ctx.projectDir,
Platform.IOS
).runAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir: ctx.projectDir,
@@ -16,9 +16,7 @@ export enum SelectAndroidBuildCredentialsResultType {
export class SelectAndroidBuildCredentials {
constructor(private app: AppLookupParams) {}
async runAsync(
ctx: Context
): Promise<
async runAsync(ctx: Context): Promise<
| {
resultType: SelectAndroidBuildCredentialsResultType.CREATE_REQUEST;
result: AndroidAppBuildCredentialsMetadataInput;
@@ -1,14 +1,20 @@
import { EasConfig, EasJsonReader } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { Context } from '../context';
export class SelectBuildProfileFromEasJson {
constructor(private easJsonReader: EasJsonReader) {}
async runAsync(ctx: Context): Promise<EasConfig> {
export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonReader: EasJsonReader;
constructor(private projectDir: string, private platform: T) {
this.easJsonReader = new EasJsonReader(projectDir);
}
async runAsync(ctx: Context): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
const easConfig = await this.easJsonReader.readAsync(profileName);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(profileName, this.platform);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
@@ -8,8 +8,8 @@ export class SelectIosDistributionTypeGraphqlFromBuildProfile {
constructor(private buildProfile: IosBuildProfile) {}
async runAsync(ctx: Context): Promise<IosDistributionTypeGraphql> {
const { distribution } = this.buildProfile;
if (distribution === 'simulator') {
const { distribution, simulator } = this.buildProfile;
if (simulator) {
throw new Error('A simulator distribution does not require credentials to be configured.');
} else if (distribution === 'store') {
return IosDistributionTypeGraphql.AppStore;
@@ -7,15 +7,14 @@ import { SelectIosDistributionTypeGraphqlFromBuildProfile } from '../SelectIosDi
describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('errors with a simulator distribution', async () => {
const buildProfile = {
distribution: 'simulator',
simulator: true,
} as IosBuildProfile;
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
await expect(
selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx)
).rejects.toThrowError();
@@ -28,12 +27,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AppStore);
});
it('returns ENTERPRISE type with an internal distribution with the universal provisioningEnterprise configuration', async () => {
@@ -45,12 +42,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.Enterprise);
});
it('returns ADHOC type with an internal distribution with the adhoc provisioningEnterprise configuration', async () => {
@@ -62,12 +57,10 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const selectIosDistributionTypeGraphqlFromBuildProfileAction = new SelectIosDistributionTypeGraphqlFromBuildProfile(
buildProfile
);
const iosDistributionTypeGraphql = await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(
ctx
);
const selectIosDistributionTypeGraphqlFromBuildProfileAction =
new SelectIosDistributionTypeGraphqlFromBuildProfile(buildProfile);
const iosDistributionTypeGraphql =
await selectIosDistributionTypeGraphqlFromBuildProfileAction.runAsync(ctx);
expect(iosDistributionTypeGraphql).toBe(IosDistributionTypeGraphql.AdHoc);
});
});
@@ -26,10 +26,11 @@ async function generateDeviceRegistrationURLAsync(
accountId: string,
appleTeam: Pick<AppleTeam, 'id'>
) {
const appleDeviceRegistrationRequest = await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
appleTeam.id,
accountId
);
const appleDeviceRegistrationRequest =
await AppleDeviceRegistrationRequestMutation.createAppleDeviceRegistrationRequestAsync(
appleTeam.id,
accountId
);
return formatRegistrationURL(appleDeviceRegistrationRequest.id);
}
+73 -35
View File
@@ -436,6 +436,8 @@ export type App = Project & {
buildOrBuildJobs: Array<BuildOrBuildJob>;
/** EAS Submissions associated with this app */
submissions: Array<Submission>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
/** iOS app credentials for the project */
iosAppCredentials: Array<IosAppCredentials>;
/** Android app credentials for the project */
@@ -487,6 +489,7 @@ export type App = Project & {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppBuildsArgs = {
filter?: Maybe<BuildFilter>;
offset: Scalars['Int'];
limit: Scalars['Int'];
status?: Maybe<BuildStatus>;
@@ -517,6 +520,14 @@ export type AppSubmissionsArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
buildListMaxSize: Scalars['Int'];
mostRecentlyUpdatedAt?: Maybe<Scalars['DateTime']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppIosAppCredentialsArgs = {
filter?: Maybe<IosAppCredentialsFilter>;
@@ -605,6 +616,25 @@ export enum AppPrivacy {
Hidden = 'HIDDEN'
}
export type BuildFilter = {
platform?: Maybe<AppPlatform>;
status?: Maybe<BuildStatus>;
distribution?: Maybe<DistributionType>;
channel?: Maybe<Scalars['String']>;
appVersion?: Maybe<Scalars['String']>;
appBuildVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
appIdentifier?: Maybe<Scalars['String']>;
buildProfile?: Maybe<Scalars['String']>;
gitCommitHash?: Maybe<Scalars['String']>;
};
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
}
export enum BuildStatus {
New = 'NEW',
InQueue = 'IN_QUEUE',
@@ -614,9 +644,10 @@ export enum BuildStatus {
Canceled = 'CANCELED'
}
export enum AppPlatform {
Ios = 'IOS',
Android = 'ANDROID'
export enum DistributionType {
Store = 'STORE',
Internal = 'INTERNAL',
Simulator = 'SIMULATOR'
}
/** Represents an EAS Build */
@@ -629,6 +660,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
initiatingActor?: Maybe<Actor>;
cancelingActor?: Maybe<Actor>;
artifacts?: Maybe<BuildArtifacts>;
logFiles: Array<Scalars['String']>;
updatedAt?: Maybe<Scalars['DateTime']>;
@@ -834,12 +866,6 @@ export type BuildMetrics = {
buildDuration?: Maybe<Scalars['Int']>;
};
export enum DistributionType {
Store = 'STORE',
Internal = 'INTERNAL',
Simulator = 'SIMULATOR'
}
export enum BuildIosEnterpriseProvisioning {
Adhoc = 'ADHOC',
Universal = 'UNIVERSAL'
@@ -984,6 +1010,39 @@ export type SubmissionFilter = {
status?: Maybe<SubmissionStatus>;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = {
__typename?: 'Deployment';
id?: Maybe<Scalars['String']>;
runtimeVersion?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
recentBuilds?: Maybe<Array<Maybe<Build>>>;
branchToReceive?: Maybe<UpdateBranch>;
};
export type UpdateBranch = {
__typename?: 'UpdateBranch';
id: Scalars['ID'];
appId: Scalars['ID'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
mostRecentUpdateForEachPlatform: Array<Update>;
};
export type UpdateBranchUpdatesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
filter?: Maybe<UpdatesFilter>;
};
export type UpdatesFilter = {
platform?: Maybe<AppPlatform>;
runtimeVersions?: Maybe<Array<Scalars['String']>>;
};
export type IosAppCredentialsFilter = {
appleAppIdentifierId?: Maybe<Scalars['String']>;
};
@@ -1239,28 +1298,6 @@ export type UpdateChannelUpdateBranchesArgs = {
limit: Scalars['Int'];
};
export type UpdateBranch = {
__typename?: 'UpdateBranch';
id: Scalars['ID'];
appId: Scalars['ID'];
name: Scalars['String'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
updates: Array<Update>;
};
export type UpdateBranchUpdatesArgs = {
offset: Scalars['Int'];
limit: Scalars['Int'];
filter?: Maybe<UpdatesFilter>;
};
export type UpdatesFilter = {
platform?: Maybe<AppPlatform>;
runtimeVersions?: Maybe<Array<Scalars['String']>>;
};
export type EnvironmentSecret = {
__typename?: 'EnvironmentSecret';
id: Scalars['ID'];
@@ -4630,6 +4667,7 @@ export type CreateAndroidBuildMutation = (
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
& BuildFragment
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
@@ -4654,6 +4692,7 @@ export type CreateIosBuildMutation = (
& { build: (
{ __typename?: 'Build' }
& Pick<Build, 'id'>
& BuildFragment
), deprecationInfo?: Maybe<(
{ __typename?: 'EASBuildDeprecationInfo' }
& Pick<EasBuildDeprecationInfo, 'type' | 'message'>
@@ -4854,8 +4893,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
appId: Scalars['String'];
offset: Scalars['Int'];
limit: Scalars['Int'];
status?: Maybe<BuildStatus>;
platform?: Maybe<AppPlatform>;
filter?: Maybe<BuildFilter>;
}>;
@@ -5078,10 +5116,10 @@ export type BuildFragment = (
& Pick<Robot, 'firstName' | 'id'>
)>, project: (
{ __typename: 'Snack' }
& Pick<Snack, 'id'>
& Pick<Snack, 'id' | 'name'>
) | (
{ __typename: 'App' }
& Pick<App, 'id'>
& Pick<App, 'id' | 'name'>
& { ownerAccount: (
{ __typename?: 'Account' }
& Pick<Account, 'id' | 'name'>
@@ -1,9 +1,11 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import nullthrows from 'nullthrows';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
AndroidJobInput,
BuildFragment,
BuildMetadataInput,
CreateAndroidBuildMutation,
CreateAndroidBuildMutationVariables,
@@ -12,9 +14,10 @@ import {
EasBuildDeprecationInfo,
IosJobInput,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
export interface BuildResult {
build: { id: string };
build: BuildFragment;
deprecationInfo?: EasBuildDeprecationInfo | null;
}
@@ -37,6 +40,7 @@ const BuildMutation = {
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
...BuildFragment
}
deprecationInfo {
type
@@ -45,6 +49,7 @@ const BuildMutation = {
}
}
}
${print(BuildFragmentNode)}
`,
input
)
@@ -70,6 +75,7 @@ const BuildMutation = {
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
build {
id
...BuildFragment
}
deprecationInfo {
type
@@ -78,6 +84,7 @@ const BuildMutation = {
}
}
}
${print(BuildFragmentNode)}
`,
input
)
@@ -8,16 +8,28 @@ import {
BuildStatus,
BuildsByIdQuery,
BuildsByIdQueryVariables,
DistributionType,
GetAllBuildsForAppQuery,
GetAllBuildsForAppQueryVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
type Filters = {
platform?: AppPlatform;
status?: BuildStatus;
type BuildsQuery = {
offset?: number;
limit?: number;
filter?: {
platform?: AppPlatform;
status?: BuildStatus;
distribution?: DistributionType;
channel?: string;
appVersion?: string;
appBuildVersion?: string;
sdkVersion?: string;
runtimeVersion?: string;
appIdentifier?: string;
buildProfile?: string;
gitCommitHash?: string;
};
};
export const BuildQuery = {
@@ -52,7 +64,7 @@ export const BuildQuery = {
async allForAppAsync(
appId: string,
{ limit = 10, offset = 0, status, platform }: Filters
{ limit = 10, offset = 0, filter }: BuildsQuery
): Promise<BuildFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -63,13 +75,12 @@ export const BuildQuery = {
$appId: String!
$offset: Int!
$limit: Int!
$status: BuildStatus
$platform: AppPlatform
$filter: BuildFilter
) {
app {
byId(appId: $appId) {
id
builds(offset: $offset, limit: $limit, status: $status, platform: $platform) {
builds(offset: $offset, limit: $limit, filter: $filter) {
id
...BuildFragment
}
@@ -78,7 +89,7 @@ export const BuildQuery = {
}
${print(BuildFragmentNode)}
`,
{ appId, offset, limit, status, platform }
{ appId, offset, limit, filter }
)
.toPromise()
);
@@ -27,6 +27,7 @@ export const BuildFragmentNode = gql`
project {
__typename
id
name
... on App {
ownerAccount {
id
@@ -309,7 +309,9 @@ describe(filterOutAssetsThatAlreadyExistAsync, () => {
});
expect(
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
await (
await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])
).length
).toBe(1);
});
it('ignores an asset that exists', async () => {
@@ -323,7 +325,9 @@ describe(filterOutAssetsThatAlreadyExistAsync, () => {
];
});
expect(
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
await (
await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])
).length
).toBe(0);
});
});
@@ -0,0 +1,24 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
interface Options {
env?: Env;
isPublicConfig?: boolean;
}
export function getExpoConfig(projectDir: string, opts: Options = {}): ExpoConfig {
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
try {
process.env = {
...process.env,
...opts.env,
};
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
});
return exp;
} finally {
process.env = originalProcessEnv;
}
}
+6 -1
View File
@@ -34,7 +34,7 @@ export async function resolveXcodeBuildContextAsync(
return {
buildScheme,
buildConfiguration:
buildProfile.schemeBuildConfiguration ??
buildProfile.buildConfiguration ??
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
projectDir,
buildScheme
@@ -63,6 +63,11 @@ export async function selectSchemeAsync({
nonInteractive?: boolean;
}): Promise<string> {
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir);
if (schemes.length === 0) {
throw new Error(
`We did not find any schemes in the Xcode project, make sure that at least one scheme is marked as "shared" in Xcode, and that it's listed in the output of "xcodebuild -list" command`
);
}
if (schemes.length === 1) {
return schemes[0];
}
@@ -21,12 +21,14 @@ export async function resolveTargetsAsync(
result.push({
targetName: applicationTarget.name,
bundleIdentifier,
buildConfiguration,
});
if (applicationTarget.dependencies && applicationTarget.dependencies.length > 0) {
for (const dependency of applicationTarget.dependencies) {
result.push({
targetName: dependency.name,
buildConfiguration,
bundleIdentifier: await getBundleIdentifierAsync(projectDir, exp, {
targetName: dependency.name,
buildConfiguration,
@@ -23,10 +23,10 @@ export async function isEasEnabledForProjectAsync(projectId: string): Promise<bo
}
}
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
'https://expo.io/eas'
)}`;
export function warnEasUnavailable() {
Log.warn(
`Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
'https://expo.io/eas'
)}`
);
Log.warn(EAS_UNAVAILABLE_MESSAGE);
}
@@ -0,0 +1,78 @@
import { exit } from '@oclif/errors';
import chalk from 'chalk';
import type MetroConfig from 'metro-config';
import resolveFrom from 'resolve-from';
import { BuildContext } from '../build/context';
import { Platform } from '../build/types';
import Log, { learnMore } from '../log';
import { confirmAsync } from '../prompts';
export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildContext<Platform>) {
if (!(await configExistsAsync(ctx.projectDir))) {
return;
}
const metroConfig = await loadConfigAsync(ctx.projectDir);
const hasHashAssetFilesPlugin = metroConfig.transformer?.assetPlugins?.find((plugin: string) =>
plugin.match(/expo-asset\/tools\/hashAssetFiles/)
);
if (!hasHashAssetFilesPlugin) {
Log.warn(
`It looks like that you are using a custom ${chalk.bold(
'metro.config.js'
)} that does not extend ${chalk.bold('@expo/metro-config')}.`
);
Log.warn(
'This can result in unexpected and hard to debug issues, like missing assets in the production bundle.'
);
Log.warn(`We recommend you to abort, fix the ${chalk.bold('metro.config.js')}, and try again.`);
Log.warn(
learnMore('https://docs.expo.io/guides/customizing-metro/', {
learnMoreMessage: 'Learn more on customizing Metro',
})
);
const shouldAbort = await confirmAsync({
message: 'Would you like to abort?',
});
if (shouldAbort) {
Log.log('Aborting...');
exit();
}
}
}
function importMetroConfigFromProject(projectDir: string): typeof MetroConfig {
const resolvedPath = resolveFrom.silent(projectDir, 'metro-config');
if (!resolvedPath) {
throw new MetroConfigPackageMissingError(
'Missing package "metro-config" in the project. ' +
'This usually means `react-native` is not installed. ' +
'Please verify that dependencies in package.json include "react-native" ' +
'and run `yarn` or `npm install`.'
);
}
return require(resolvedPath);
}
async function configExistsAsync(projectRoot: string): Promise<boolean> {
try {
const MetroConfig = importMetroConfigFromProject(projectRoot);
const result = await MetroConfig.resolveConfig(undefined, projectRoot);
return !result.isEmpty;
} catch (err) {
if (err instanceof MetroConfigPackageMissingError) {
return false;
} else {
throw err;
}
}
}
async function loadConfigAsync(projectDir: string): Promise<MetroConfig.ConfigT> {
const MetroConfig = importMetroConfigFromProject(projectDir);
return await MetroConfig.loadConfig({ cwd: projectDir }, {});
}
class MetroConfigPackageMissingError extends Error {}
+14 -11
View File
@@ -1,10 +1,5 @@
import {
AppJSONConfig,
ExpoConfig,
getConfig,
getConfigFilePaths,
modifyConfigAsync,
} from '@expo/config';
import { AppJSONConfig, ExpoConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import gql from 'graphql-tag';
import path from 'path';
@@ -20,6 +15,7 @@ import {
ensureProjectExistsAsync,
findProjectIdByAccountNameAndSlugNullableAsync,
} from './ensureProjectExists';
import { getExpoConfig } from './expoConfig';
export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
switch (user.__typename) {
@@ -61,8 +57,11 @@ export async function findProjectRootAsync(cwd?: string): Promise<string | null>
return projectRootDir ?? null;
}
export async function setProjectIdAsync(projectDir: string): Promise<ExpoConfig | undefined> {
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
export async function setProjectIdAsync(
projectDir: string,
options: { env?: Env } = {}
): Promise<ExpoConfig | undefined> {
const exp = getExpoConfig(projectDir, options);
const privacy = toAppPrivacy(exp.privacy);
const projectId = await ensureProjectExistsAsync({
@@ -111,7 +110,10 @@ export async function setProjectIdAsync(projectDir: string): Promise<ExpoConfig
return (result.config as AppJSONConfig)?.expo;
}
export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
export async function getProjectIdAsync(
exp: ExpoConfig,
options: { env?: Env } = {}
): Promise<string> {
if (!process.env.EAS_ENABLE_PROJECT_ID) {
const privacy = toAppPrivacy(exp.privacy);
return await ensureProjectExistsAsync({
@@ -120,6 +122,7 @@ export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
privacy,
});
}
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
return localProjectId;
@@ -130,7 +133,7 @@ export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const newExp = await setProjectIdAsync(projectDir);
const newExp = await setProjectIdAsync(projectDir, options);
const newLocalProjectId = newExp?.extra?.eas?.projectId;
if (!newLocalProjectId) {
+1 -1
View File
@@ -14,7 +14,7 @@ export async function resolveWorkflowAsync(
platformWorkflowMarker =
platform === Platform.ANDROID
? await AndroidConfig.Paths.getAndroidManifestAsync(projectDir)
: IOSConfig.Paths.getXcodeProjectPath(projectDir);
: IOSConfig.Paths.getPBXProjectPath(projectDir);
} catch {
return Workflow.MANAGED;
}
@@ -26,7 +26,7 @@ async function startSubmissionAsync(
const { submission } = await SubmissionMutation.createSubmissionAsync({
appId: projectId,
platform,
config: (config as unknown) as JSONObject,
config: config as unknown as JSONObject,
submittedBuildId: buildId,
});
return submission.id;
@@ -155,7 +155,7 @@ class AndroidSubmitCommand {
});
} else {
return result({
sourceType: ServiceAccountSourceType.prompt,
sourceType: ServiceAccountSourceType.detect,
});
}
}
@@ -1,13 +1,18 @@
import chalk from 'chalk';
import glob from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { filterAsync } from '../../utils/filterAsync';
import { isExistingFile } from '../utils/files';
export enum ServiceAccountSourceType {
path,
prompt,
detect,
// credentialsService,
// ...
}
@@ -25,7 +30,14 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.prompt;
}
export type ServiceAccountSource = ServiceAccountPathSource | ServiceAccountPromptSource;
interface ServiceAccountDetectSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.detect;
}
export type ServiceAccountSource =
| ServiceAccountPathSource
| ServiceAccountPromptSource
| ServiceAccountDetectSource;
export async function getServiceAccountAsync(source: ServiceAccountSource): Promise<string> {
switch (source.sourceType) {
@@ -33,6 +45,8 @@ export async function getServiceAccountAsync(source: ServiceAccountSource): Prom
return await handlePathSourceAsync(source);
case ServiceAccountSourceType.prompt:
return await handlePromptSourceAsync(source);
case ServiceAccountSourceType.detect:
return await handleDetectSourceAsync(source);
}
}
@@ -44,6 +58,35 @@ async function handlePathSourceAsync(source: ServiceAccountPathSource): Promise<
return source.path;
}
async function handleDetectSourceAsync(_source: ServiceAccountDetectSource): Promise<string> {
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const foundFilePaths = await glob('**/*.json', {
cwd: projectDir,
ignore: ['app.json', 'package*.json', 'tsconfig.json', 'node_modules'],
});
const googleServiceFiles = await filterAsync(
foundFilePaths.map(file => path.join(projectDir, file)),
fileIsGoogleServicesAsync
);
if (googleServiceFiles.length > 1) {
const selectedPath = await displayPathChooserAsync(googleServiceFiles, projectDir);
if (selectedPath !== false) {
return selectedPath;
}
} else if (googleServiceFiles.length === 1) {
const [detectedPath] = googleServiceFiles;
if (await confirmDetectedPathAsync(detectedPath)) {
return detectedPath;
}
}
return await getServiceAccountAsync({ sourceType: ServiceAccountSourceType.prompt });
}
async function handlePromptSourceAsync(_source: ServiceAccountPromptSource): Promise<string> {
const path = await askForServiceAccountPathAsync();
return await getServiceAccountAsync({
@@ -80,3 +123,53 @@ async function askForServiceAccountPathAsync(): Promise<string> {
});
return filePath;
}
async function displayPathChooserAsync(
paths: string[],
projectDir: string
): Promise<string | false> {
const choices = paths.map<{ title: string; value: string | false }>(f => ({
value: f,
title: f.startsWith(projectDir) ? path.relative(projectDir, f) : f,
}));
choices.push({
title: 'None of the above',
value: false,
});
Log.log(
'Multiple Google Service Account JSON keys have been found inside your project directory.'
);
const { selectedPath } = await promptAsync({
name: 'selectedPath',
type: 'select',
message: 'Choose the key you want to use for this submission:',
choices,
});
Log.addNewLineIfNone();
return selectedPath;
}
async function confirmDetectedPathAsync(path: string): Promise<boolean> {
Log.log(`A Google Service Account JSON key has been found at\n ${chalk.underline(path)}`);
const { confirmed } = await promptAsync({
name: 'confirmed',
type: 'confirm',
message: 'Would you like to use this file?',
initial: true,
});
Log.addNewLineIfNone();
return confirmed;
}
async function fileIsGoogleServicesAsync(path: string): Promise<boolean> {
try {
const jsonFile = await fs.readJson(path);
return jsonFile.type === 'service_account';
} catch (e) {
return false;
}
}
@@ -1,6 +1,7 @@
import { vol } from 'memfs';
import { asMock } from '../../../__tests__/utils';
import { findProjectRootAsync } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import {
ServiceAccountSource,
@@ -10,11 +11,19 @@ import {
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../project/projectUtils');
describe(getServiceAccountAsync, () => {
beforeAll(() => {
const mockDetectableServiceAccountJson = JSON.stringify({
type: 'service_account',
});
vol.fromJSON({
'/google-service-account.json': JSON.stringify({ service: 'account' }),
'/project_dir/subdir/service-account.json': mockDetectableServiceAccountJson,
'/project_dir/another-service-account.json': mockDetectableServiceAccountJson,
'/other_dir/invalid_file.txt': 'this is not even a JSON',
});
});
afterAll(() => {
@@ -90,4 +99,100 @@ describe(getServiceAccountAsync, () => {
expect(serviceAccountPath).toBe('/google-service-account.json');
});
});
describe('when source is ServiceAccountSourceType.detect', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
asMock(promptAsync).mockResolvedValueOnce({
confirmed: true,
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
expect(serviceAccountPath).toBe('/project_dir/subdir/service-account.json');
});
it('falls back to prompt, when no valid files are found in the dir', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/other_dir'); // no valid files in that dir
asMock(promptAsync).mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
});
it('falls back to prompt, when user rejects to use detected file', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir/subdir'); // should find 1 file
asMock(promptAsync)
.mockResolvedValueOnce({
confirmed: false,
})
.mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.detect,
};
const serviceAccountPath = await getServiceAccountAsync(source);
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'confirm' }));
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
});
it('displays a chooser, when multiple files are found', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
asMock(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
const expectedChoices = expect.arrayContaining([
expect.objectContaining({ value: '/project_dir/another-service-account.json' }),
expect.objectContaining({ value: '/project_dir/subdir/service-account.json' }),
expect.objectContaining({ value: false }),
]);
expect(promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
type: 'select',
choices: expectedChoices,
})
);
expect(serviceAccountPath).toBe('/project_dir/another-service-account.json');
});
it('falls back to prompt, when user selects the "None of above"', async () => {
asMock(findProjectRootAsync).mockResolvedValueOnce('/project_dir'); // should find 2 files here
asMock(promptAsync)
.mockResolvedValueOnce({
selectedPath: false,
})
.mockResolvedValueOnce({
filePath: '/google-service-account.json',
});
const serviceAccountPath = await getServiceAccountAsync({
sourceType: ServiceAccountSourceType.detect,
});
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ type: 'select' }));
expect(promptAsync).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'text' }));
expect(serviceAccountPath).toBe('/google-service-account.json');
});
});
});
@@ -31,9 +31,11 @@ export async function getLatestBuildForSubmissionAsync(
appId: string
): Promise<BuildFragmentWithArtifact | null> {
const builds = await BuildQuery.allForAppAsync(appId, {
platform,
status: BuildStatus.Finished,
limit: 1,
filter: {
platform,
status: BuildStatus.Finished,
},
});
if (builds.length < 1) {
+10
View File
@@ -0,0 +1,10 @@
/**
* asynchronous array filter
* @param arr array to filter
* @param predicate a predicate function to run asynchronously
* @returns a promise resolving to a filtered array
*/
export const filterAsync = async <T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => Promise<boolean>
) => Promise.all(arr.map(predicate)).then(results => arr.filter((_v, index) => results[index]));
+4 -5
View File
@@ -1,14 +1,14 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.22.0",
"version": "0.22.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.44",
"@expo/eas-build-job": "0.2.45",
"@hapi/joi": "17.1.1",
"fs-extra": "9.0.1",
"tslib": "1.14.1"
"tslib": "2.3.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
@@ -20,8 +20,7 @@
"@types/node": "^12",
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"ts-node": "^8",
"typescript": "^3.3"
"typescript": "4.3.5"
},
"engines": {
"node": ">=8.0.0"
@@ -0,0 +1,190 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import {
AndroidBuildProfile,
BuildProfile,
EasConfig,
IosBuildProfile,
} from './DeprecatedConfig.types';
import { EasJsonSchema, schemaBuildProfileMap } from './DeprecatedEasJsonSchema';
export interface EasJson {
builds: {
android?: { [key: string]: BuildProfilePreValidation };
ios?: { [key: string]: BuildProfilePreValidation };
};
}
interface BuildProfilePreValidation {
workflow?: Workflow;
extends?: string;
}
function intersect<T>(setA: Set<T>, setB: Set<T>): Set<T> {
return new Set([...setA].filter(i => setB.has(i)));
}
export class EasJsonReader {
constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
/**
* Return build profile names for a particular platform.
* If platform is 'all', return common build profiles for all platforms
*/
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readRawAsync();
if (this.platform === 'android') {
return Object.keys(easJson?.builds?.android ?? {});
} else if (this.platform === 'ios') {
return Object.keys(easJson?.builds?.ios ?? {});
} else {
const intersectingProfileNames = intersect(
new Set(Object.keys(easJson?.builds?.ios ?? {})),
new Set(Object.keys(easJson?.builds?.android ?? {}))
);
return Array.from(intersectingProfileNames);
}
}
public async readAsync(buildProfileName: string): Promise<EasConfig> {
const easJson = await this.readRawAsync();
let androidConfig;
if (['android', 'all'].includes(this.platform)) {
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
Platform.ANDROID,
buildProfileName,
easJson.builds?.android || {}
);
}
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile<IosBuildProfile>(
Platform.IOS,
buildProfileName,
easJson.builds?.ios || {}
);
}
return {
builds: {
...(androidConfig ? { android: androidConfig } : {}),
...(iosConfig ? { ios: iosConfig } : {}),
},
};
}
public async validateAsync(): Promise<void> {
const easJson = await this.readRawAsync();
const androidProfiles = easJson.builds?.android ?? {};
for (const name of Object.keys(androidProfiles)) {
try {
this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
} catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
throw err;
}
}
const iosProfiles = easJson.builds?.ios ?? {};
for (const name of Object.keys(iosProfiles)) {
try {
this.validateBuildProfile(Platform.IOS, name, iosProfiles);
} catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
throw err;
}
}
}
public async readRawAsync(): Promise<EasJson> {
const rawFile = await fs.readFile(path.join(this.projectDir, 'eas.json'), 'utf8');
const json = JSON.parse(rawFile);
const { value, error } = EasJsonSchema.validate(json, {
abortEarly: false,
});
if (error) {
throw new Error(`eas.json is not valid [${error.toString()}]`);
}
return value;
}
private validateBuildProfile<T extends BuildProfile>(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>
): T {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
const schema = schemaBuildProfileMap[platform];
const { value, error } = schema.validate(buildProfile, {
stripUnknown: true,
convert: true,
abortEarly: false,
});
if (error) {
throw new Error(
`Object "${platform}.${buildProfileName}" in eas.json is not valid [${error.toString()}]`
);
}
return value;
}
private resolveBuildProfile(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>,
depth: number = 0
): Record<string, any> {
if (depth >= 2) {
throw new Error(
'Too long chain of build profile extensions, make sure "extends" keys do not make a cycle'
);
}
const buildProfile = buildProfiles[buildProfileName];
if (!buildProfile) {
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
}
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
if (baseProfileName) {
return deepMerge(
this.resolveBuildProfile(platform, baseProfileName, buildProfiles, depth + 1),
buildProfileRest
);
} else {
return buildProfileRest;
}
}
}
function isObject(value: any): boolean {
return typeof value === 'object' && value !== null;
}
export function deepMerge(
base: Record<string, any>,
update: Record<string, any>
): Record<string, any> {
const result: Record<string, any> = {};
Object.keys(base).forEach(key => {
const oldValue = base[key];
const newValue = update[key];
if (isObject(newValue) && isObject(oldValue)) {
result[key] = deepMerge(oldValue, newValue);
} else if (newValue !== undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
} else {
result[key] = isObject(oldValue) ? deepMerge({}, oldValue) : oldValue;
}
});
Object.keys(update).forEach(key => {
const newValue = update[key];
if (result[key] === undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
}
});
return result;
}
@@ -0,0 +1,95 @@
import { Android, Ios } from '@expo/eas-build-job';
import Joi, { CustomHelpers } from '@hapi/joi';
const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
return value;
} else {
throw new Error(`${value} is not a valid version`);
}
};
const AndroidBuilderEnvironmentSchema = Joi.object({
image: Joi.string()
.valid(...Android.builderBaseImages)
.default('default'),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const IosBuilderEnvironmentSchema = Joi.object({
image: Joi.string().valid(...Ios.builderBaseImages),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const CacheSchema = Joi.object({
disabled: Joi.boolean().default(false),
key: Joi.string().max(128),
cacheDefaultPaths: Joi.boolean().default(true),
customPaths: Joi.array().items(Joi.string()).default([]),
});
const AndroidSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
withoutCredentials: Joi.boolean().default(false),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
buildType: Joi.alternatives().conditional('distribution', {
is: 'internal',
then: Joi.string().valid('apk', 'development-client'),
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client'),
}),
}).concat(AndroidBuilderEnvironmentSchema);
const IosSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
artifactPath: Joi.string(),
scheme: Joi.string(),
schemeBuildConfiguration: Joi.string(),
buildType: Joi.string().valid('release', 'development-client'),
}).concat(IosBuilderEnvironmentSchema);
export const schemaBuildProfileMap: Record<string, Joi.Schema> = {
android: AndroidSchema,
ios: IosSchema,
};
export const EasJsonSchema = Joi.object({
builds: Joi.object({
android: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
ios: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
}),
});
+71
View File
@@ -0,0 +1,71 @@
import { Android, Cache, Ios, Platform } from '@expo/eas-build-job';
export enum CredentialsSource {
LOCAL = 'local',
REMOTE = 'remote',
}
export type DistributionType = 'store' | 'internal';
export type IosEnterpriseProvisioning = 'adhoc' | 'universal';
export type VersionAutoIncrement = boolean | 'version' | 'buildNumber';
export interface CommonBuildProfile {
credentialsSource: CredentialsSource;
distribution: DistributionType;
cache?: Omit<Cache, 'clear'>;
releaseChannel?: string;
channel?: string;
developmentClient?: boolean;
node?: string;
yarn?: string;
expoCli?: string;
env?: Record<string, string>;
}
export interface AndroidBuildProfile extends CommonBuildProfile {
withoutCredentials?: boolean;
image?: Android.BuilderEnvironment['image'];
ndk?: string;
buildType?: Android.BuildType.APK | Android.BuildType.APP_BUNDLE;
gradleCommand?: string;
artifactPath?: string;
}
export interface IosBuildProfile extends CommonBuildProfile {
enterpriseProvisioning?: IosEnterpriseProvisioning;
autoIncrement?: VersionAutoIncrement;
simulator?: boolean;
image?: Ios.BuilderEnvironment['image'];
bundler?: string;
fastlane?: string;
cocoapods?: string;
artifactPath?: string;
scheme?: string;
buildConfiguration?: string;
}
export type BuildProfile<TPlatform extends Platform = Platform> = TPlatform extends Platform.ANDROID
? AndroidBuildProfile
: TPlatform extends Platform.IOS
? IosBuildProfile
: TPlatform extends Platform
? AndroidBuildProfile | IosBuildProfile
: never;
export interface RawBuildProfile extends Partial<CommonBuildProfile> {
extends?: string;
android?: Partial<AndroidBuildProfile>;
ios?: Partial<IosBuildProfile>;
}
export interface EasJson {
build: {
[profile: string]: RawBuildProfile;
};
}
+80 -133
View File
@@ -1,103 +1,75 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { AndroidBuildProfile, BuildProfile, EasConfig, IosBuildProfile } from './Config.types';
import { EasJsonSchema, schemaBuildProfileMap } from './EasJsonSchema';
import { BuildProfile, CredentialsSource, EasJson, RawBuildProfile } from './EasJson.types';
import { EasJsonSchema, MinimalEasJsonSchema } from './EasJsonSchema';
interface EasJson {
builds: {
android?: { [key: string]: BuildProfilePreValidation };
ios?: { [key: string]: BuildProfilePreValidation };
};
interface EasJsonPreValidation {
build: { [profile: string]: object };
}
interface BuildProfilePreValidation {
workflow?: Workflow;
extends?: string;
}
function intersect<T>(setA: Set<T>, setB: Set<T>): Set<T> {
return new Set([...setA].filter(i => setB.has(i)));
}
const defaults = {
distribution: 'store',
credentialsSource: CredentialsSource.REMOTE,
} as const;
export class EasJsonReader {
constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
public static formatEasJsonPath(projectDir: string) {
return path.join(projectDir, 'eas.json');
}
constructor(private projectDir: string) {}
/**
* Return build profile names for a particular platform.
* If platform is 'all', return common build profiles for all platforms
*/
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readRawAsync();
if (this.platform === 'android') {
return Object.keys(easJson?.builds?.android ?? {});
} else if (this.platform === 'ios') {
return Object.keys(easJson?.builds?.ios ?? {});
return Object.keys(easJson?.build ?? {});
}
public async readBuildProfileAsync<T extends Platform>(
buildProfileName: string,
platform: T
): Promise<BuildProfile<T>> {
const easJson = await this.readAndValidateAsync();
this.ensureProfileExists(easJson, buildProfileName);
const {
android: resolvedAndroidSpecificValues,
ios: resolvedIosSpecificValues,
...resolvedProfile
} = this.resolveBuildProfile(easJson, buildProfileName);
if (platform === Platform.ANDROID) {
const profileWithoutDefaults = profileMerge(
resolvedProfile,
resolvedAndroidSpecificValues ?? {}
);
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
} else if (platform === Platform.IOS) {
const profileWithoutDefaults = profileMerge(resolvedProfile, resolvedIosSpecificValues ?? {});
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
} else {
const intersectingProfileNames = intersect(
new Set(Object.keys(easJson?.builds?.ios ?? {})),
new Set(Object.keys(easJson?.builds?.android ?? {}))
);
return Array.from(intersectingProfileNames);
throw new Error(`Unknown platform ${platform}`);
}
}
public async readAsync(buildProfileName: string): Promise<EasConfig> {
public async readAndValidateAsync(): Promise<EasJson> {
const easJson = await this.readRawAsync();
const { value, error } = EasJsonSchema.validate(easJson, {
allowUnknown: false,
convert: true,
abortEarly: false,
});
let androidConfig;
if (['android', 'all'].includes(this.platform)) {
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
Platform.ANDROID,
buildProfileName,
easJson.builds?.android || {}
);
if (error) {
throw new Error(`eas.json is not valid [${error.toString()}]`);
}
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile<IosBuildProfile>(
Platform.IOS,
buildProfileName,
easJson.builds?.ios || {}
);
}
return {
builds: {
...(androidConfig ? { android: androidConfig } : {}),
...(iosConfig ? { ios: iosConfig } : {}),
},
};
return value as EasJson;
}
public async validateAsync(): Promise<void> {
const easJson = await this.readRawAsync();
const androidProfiles = easJson.builds?.android ?? {};
for (const name of Object.keys(androidProfiles)) {
try {
this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
} catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
throw err;
}
}
const iosProfiles = easJson.builds?.ios ?? {};
for (const name of Object.keys(iosProfiles)) {
try {
this.validateBuildProfile(Platform.IOS, name, iosProfiles);
} catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
throw err;
}
}
}
public async readRawAsync(): Promise<EasJson> {
const rawFile = await fs.readFile(path.join(this.projectDir, 'eas.json'), 'utf8');
public async readRawAsync(): Promise<EasJsonPreValidation> {
const rawFile = await fs.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
const json = JSON.parse(rawFile);
const { value, error } = EasJsonSchema.validate(json, {
const { value, error } = MinimalEasJsonSchema.validate(json, {
abortEarly: false,
});
@@ -107,79 +79,54 @@ export class EasJsonReader {
return value;
}
private validateBuildProfile<T extends BuildProfile>(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>
): T {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
const schema = schemaBuildProfileMap[platform];
const { value, error } = schema.validate(buildProfile, {
stripUnknown: true,
convert: true,
abortEarly: false,
});
if (error) {
throw new Error(
`Object "${platform}.${buildProfileName}" in eas.json is not valid [${error.toString()}]`
);
}
return value;
}
private resolveBuildProfile(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>,
easJson: EasJson,
profileName: string,
depth: number = 0
): Record<string, any> {
): RawBuildProfile {
if (depth >= 2) {
throw new Error(
'Too long chain of build profile extensions, make sure "extends" keys do not make a cycle'
);
}
const buildProfile = buildProfiles[buildProfileName];
const buildProfile = easJson.build[profileName];
if (!buildProfile) {
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
throw new Error(`There is no profile named ${profileName}`);
}
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
if (baseProfileName) {
return deepMerge(
this.resolveBuildProfile(platform, baseProfileName, buildProfiles, depth + 1),
return profileMerge(
this.resolveBuildProfile(easJson, baseProfileName, depth + 1),
buildProfileRest
);
} else {
return buildProfileRest;
}
}
private ensureProfileExists(easJson: EasJson, profileName: string) {
if (!easJson.build || !easJson.build[profileName]) {
throw new Error(`There is no profile named ${profileName} in eas.json.`);
}
}
}
function isObject(value: any): boolean {
return typeof value === 'object' && value !== null;
}
export function deepMerge(
base: Record<string, any>,
update: Record<string, any>
): Record<string, any> {
const result: Record<string, any> = {};
Object.keys(base).forEach(key => {
const oldValue = base[key];
const newValue = update[key];
if (isObject(newValue) && isObject(oldValue)) {
result[key] = deepMerge(oldValue, newValue);
} else if (newValue !== undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
} else {
result[key] = isObject(oldValue) ? deepMerge({}, oldValue) : oldValue;
}
});
Object.keys(update).forEach(key => {
const newValue = update[key];
if (result[key] === undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
}
});
export function profileMerge(base: RawBuildProfile, update: RawBuildProfile): RawBuildProfile {
const result = {
...base,
...update,
};
if (base.env && update.env) {
result.env = {
...base.env,
...update.env,
};
}
if (base.android && update.android) {
result.android = profileMerge(base.android, update.android);
}
if (base.ios && update.ios) {
result.ios = profileMerge(base.ios, update.ios);
}
return result;
}
+56 -71
View File
@@ -1,7 +1,7 @@
import { Android, Ios } from '@expo/eas-build-job';
import Joi, { CustomHelpers } from '@hapi/joi';
import Joi from '@hapi/joi';
const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
const semverSchemaCheck = (value: any) => {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
return value;
} else {
@@ -9,87 +9,72 @@ const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
}
};
const AndroidBuilderEnvironmentSchema = Joi.object({
image: Joi.string()
.valid(...Android.builderBaseImages)
.default('default'),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const IosBuilderEnvironmentSchema = Joi.object({
image: Joi.string().valid(...Ios.builderBaseImages),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
});
const CacheSchema = Joi.object({
disabled: Joi.boolean().default(false),
disabled: Joi.boolean(),
key: Joi.string().max(128),
cacheDefaultPaths: Joi.boolean().default(true),
customPaths: Joi.array().items(Joi.string()).default([]),
cacheDefaultPaths: Joi.boolean(),
customPaths: Joi.array().items(Joi.string()),
});
const AndroidSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
const CommonBuildProfileSchema = Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
cache: CacheSchema,
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
withoutCredentials: Joi.boolean().default(false),
developmentClient: Joi.boolean(),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
});
buildType: Joi.alternatives().conditional('distribution', {
is: 'internal',
then: Joi.string().valid('apk', 'development-client'),
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client'),
}),
}).concat(AndroidBuilderEnvironmentSchema);
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
withoutCredentials: Joi.boolean(),
const IosSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
image: Joi.string().valid(...Android.builderBaseImages),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
artifactPath: Joi.string(),
scheme: Joi.string(),
schemeBuildConfiguration: Joi.string(),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
buildType: Joi.string().valid('release', 'development-client'),
}).concat(IosBuilderEnvironmentSchema);
buildType: Joi.string().valid('apk', 'app-bundle'),
})
);
export const schemaBuildProfileMap: Record<string, Joi.Schema> = {
android: AndroidSchema,
ios: IosSchema,
};
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'buildNumber')
),
image: Joi.string().valid(...Ios.builderBaseImages),
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
artifactPath: Joi.string(),
scheme: Joi.string(),
buildConfiguration: Joi.string(),
})
);
const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
extends: Joi.string(),
android: AndroidBuildProfileSchema,
ios: IosBuildProfileSchema,
})
);
export const MinimalEasJsonSchema = Joi.object({
build: Joi.object().pattern(Joi.string(), Joi.object()),
});
export const EasJsonSchema = Joi.object({
builds: Joi.object({
android: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
ios: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
}),
build: Joi.object().pattern(Joi.string(), EasJsonBuildProfileSchema),
});
@@ -1,3 +1,4 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
@@ -10,314 +11,268 @@ beforeEach(async () => {
await fs.mkdirp('/project');
});
test('minimal valid android eas.json', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('release');
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
});
test('minimal valid ios eas.json', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
},
},
});
const reader = new EasJsonReader('/project', 'ios');
const easJson = await reader.readAsync('release');
expect({
builds: {
ios: {
credentialsSource: 'remote',
distribution: 'store',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
},
},
}).toEqual(easJson);
});
test('minimal valid eas.json for both platforms', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
},
ios: {
release: {},
},
build: {
release: {},
},
});
const reader = new EasJsonReader('/project', 'all');
const easJson = await reader.readAsync('release');
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
ios: {
distribution: 'store',
credentialsSource: 'remote',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
},
},
}).toEqual(easJson);
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync('release', Platform.IOS);
const androidProfile = await reader.readBuildProfileAsync('release', Platform.ANDROID);
test('valid eas.json with both platform, but reading only android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
},
android: {
release: {},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('release');
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(androidProfile);
expect({
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(iosProfile);
});
test('valid eas.json for development client builds', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
debug: { buildType: 'development-client' },
},
android: {
release: {},
debug: {
withoutCredentials: false,
buildType: 'development-client',
build: {
release: {},
debug: {
developmentClient: true,
android: {
withoutCredentials: true,
},
},
},
});
const reader = new EasJsonReader('/project', 'all');
const easJson = await reader.readAsync('debug');
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync('debug', Platform.IOS);
const androidProfile = await reader.readBuildProfileAsync('debug', Platform.ANDROID);
expect({
builds: {
android: {
credentialsSource: 'remote',
distribution: 'store',
env: {},
image: 'default',
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
ios: {
credentialsSource: 'remote',
distribution: 'store',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
},
}).toEqual(easJson);
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
}).toEqual(androidProfile);
expect({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
}).toEqual(iosProfile);
});
test('valid generic profile for internal distribution on Android', async () => {
test('valid profile for internal distribution on Android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
distribution: 'internal',
},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('internal');
expect({
builds: {
android: {
build: {
internal: {
distribution: 'internal',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
});
test('valid managed profile for internal distribution on Android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
distribution: 'internal',
},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('internal');
const reader = new EasJsonReader('/project');
const profile = await reader.readBuildProfileAsync('internal', Platform.ANDROID);
expect({
builds: {
android: {
distribution: 'internal',
credentialsSource: 'remote',
withoutCredentials: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
distribution: 'internal',
credentialsSource: 'remote',
}).toEqual(profile);
});
test('invalid managed profile for internal distribution on Android', async () => {
test('valid profile extending other profile', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
buildType: 'aab',
distribution: 'internal',
build: {
base: {
node: '12.0.0',
},
extension: {
extends: 'base',
distribution: 'internal',
node: '13.0.0',
},
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
expect({
distribution: 'store',
credentialsSource: 'remote',
node: '12.0.0',
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
node: '13.0.0',
}).toEqual(extendedProfile);
});
test('valid profile extending other profile with platform specific envs', async () => {
await fs.writeJson('/project/eas.json', {
build: {
base: {
env: {
BASE_ENV: '1',
PROFILE: 'base',
},
},
extension: {
extends: 'base',
distribution: 'internal',
env: {
PROFILE: 'extension',
},
android: {
env: {
PROFILE: 'extension:android',
},
},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('internal');
await expect(promise).rejects.toThrowError(
'Object "android.internal" in eas.json is not valid [ValidationError: "buildType" must be one of [apk, development-client]]'
);
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
expect({
distribution: 'store',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'base',
},
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension:android',
},
}).toEqual(extendedAndroidProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension',
},
}).toEqual(extendedIosProfile);
});
test('invalid eas.json with missing preset', async () => {
test('valid profile extending other profile with platform specific caching', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
build: {
base: {
cache: {
disabled: true,
},
},
extension: {
extends: 'base',
distribution: 'internal',
cache: {
key: 'extend-key',
},
android: {
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('debug');
await expect(promise).rejects.toThrowError(
'There is no profile named debug for platform android'
);
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
expect({
distribution: 'store',
credentialsSource: 'remote',
cache: {
disabled: true,
},
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
}).toEqual(extendedAndroidProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
cache: {
key: 'extend-key',
},
}).toEqual(extendedIosProfile);
});
test('invalid eas.json when using buildType for wrong platform', async () => {
test('valid eas.json with missing profile', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { buildType: 'archive' },
},
build: {
release: {},
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('debug', Platform.ANDROID);
await expect(promise).rejects.toThrowError('There is no profile named debug in eas.json.');
});
test('invalid eas.json when using wrong buildType', async () => {
await fs.writeJson('/project/eas.json', {
build: {
release: { android: { buildType: 'archive' } },
},
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError(
'Object "android.release" in eas.json is not valid [ValidationError: "buildType" must be one of [apk, app-bundle, development-client]]'
'eas.json is not valid [ValidationError: "build.release.android.buildType" must be one of [apk, app-bundle]]'
);
});
test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
await expect(promise).rejects.toThrowError(
'There is no profile named release for platform android'
);
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError('There is no profile named release in eas.json.');
});
test('invalid semver value', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { node: '12.0.0-alpha' },
},
build: {
release: { node: '12.0.0-alpha' },
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError(
'Object "android.release" in eas.json is not valid [ValidationError: "node" failed custom validation because 12.0.0-alpha is not a valid version]'
'eas.json is not valid [ValidationError: "build.release.node" failed custom validation because 12.0.0-alpha is not a valid version]'
);
});
test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
ios: {
test: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
build: {
release: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
});
const androidReader = new EasJsonReader('/project', 'android');
const androidProfileNames = await androidReader.getBuildProfileNamesAsync();
expect(androidProfileNames.sort()).toEqual(['release', 'blah'].sort());
const iosReader = new EasJsonReader('/project', 'ios');
const iosProfileNames = await iosReader.getBuildProfileNamesAsync();
expect(iosProfileNames.sort()).toEqual(['test', 'blah'].sort());
const allReader = new EasJsonReader('/project', 'all');
const allProfileNames = await allReader.getBuildProfileNamesAsync();
expect(allProfileNames.sort()).toEqual(['blah'].sort());
const reader = new EasJsonReader('/project');
const allProfileNames = await reader.getBuildProfileNamesAsync();
expect(allProfileNames.sort()).toEqual(['blah', 'release'].sort());
});
@@ -1,56 +0,0 @@
import { deepMerge } from '../EasJsonReader';
test('deepMerge: case 1', async () => {
const result = deepMerge({ b: 'test', c: 'test2' }, { b: 'test1' });
expect(result).toEqual({ b: 'test1', c: 'test2' });
});
test('deepMerge: case 2', async () => {
const result = deepMerge({ a: { b: 'test', c: 'test2' } }, { a: { b: 'test1' } });
expect(result).toEqual({ a: { b: 'test1', c: 'test2' } });
});
test('deepMerge: case 3', async () => {
const result = deepMerge({ a: { b: 'test' } }, { a: { b: 'test1', d: 'test3' } });
expect(result).toEqual({ a: { b: 'test1', d: 'test3' } });
});
test('deepMerge: case 4', async () => {
const result = deepMerge({ a: 'test' }, { a: { b: 'test1', d: 'test3' } });
expect(result).toEqual({ a: { b: 'test1', d: 'test3' } });
});
test('deepMerge: case 5', async () => {
const result = deepMerge({ a: { b: 'test1', d: 'test3' } }, { a: 'test' });
expect(result).toEqual({ a: 'test' });
});
test('deepMerge: case 5', async () => {
const result = deepMerge({ a: { b: 'test1', d: 'test3' } }, { a: 0 });
expect(result).toEqual({ a: 0 });
});
test('deepMerge: case 6', async () => {
const result = deepMerge({ a: { b: 'test1', d: 'test3' } }, { a: null });
expect(result).toEqual({ a: null });
});
test('deepMerge: case 7', async () => {
const result = deepMerge({ a: { b: 'test1', d: 'test3' } }, { a: undefined });
expect(result).toEqual({ a: { b: 'test1', d: 'test3' } });
});
test('deepMerge: case 8', async () => {
const result = deepMerge({ a: 'test' }, { a: 0 });
expect(result).toEqual({ a: 0 });
});
test('deepMerge: case 9', async () => {
const result = deepMerge({ a: 'test' }, { a: null });
expect(result).toEqual({ a: null });
});
test('deepMerge: case 10', async () => {
const result = deepMerge({ a: 'test' }, { a: undefined });
expect(result).toEqual({ a: 'test' });
});
@@ -0,0 +1,297 @@
import { EasJson as DeprecatedEasJson } from '../DeprecatedEasJsonReader';
import { migrateProfile } from '../migrate';
test('migration for default manged eas.json', async () => {
const easJson = {
builds: {
android: {
release: {
buildType: 'app-bundle',
},
development: {
buildType: 'development-client',
distribution: 'internal',
},
},
ios: {
release: {
buildType: 'release',
},
development: {
buildType: 'development-client',
distribution: 'internal',
},
},
},
} as DeprecatedEasJson;
expect(migrateProfile(easJson, 'release')).toEqual({
android: {
buildType: 'app-bundle',
},
});
expect(migrateProfile(easJson, 'development')).toEqual({
developmentClient: true,
distribution: 'internal',
});
});
test('migration for default generic eas.json', async () => {
const easJson = {
builds: {
android: {
release: {
gradleCommand: ':app:bundleRelease',
},
development: {
gradleCommand: ':app:assembleDebug',
distribution: 'internal',
},
},
ios: {
release: {
schemeBuildConfiguration: 'Release',
},
development: {
schemeBuildConfiguration: 'Debug',
distribution: 'internal',
},
},
},
} as DeprecatedEasJson;
expect(migrateProfile(easJson, 'release')).toEqual({
android: {
gradleCommand: ':app:bundleRelease',
},
ios: {
buildConfiguration: 'Release',
},
});
expect(migrateProfile(easJson, 'development')).toEqual({
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
ios: {
buildConfiguration: 'Debug',
},
});
});
test('migration for example manged eas.json', async () => {
const easJson = {
builds: {
android: {
base: {
image: 'default',
env: {
EXAMPLE_ENV: 'example value',
},
},
release: {
extends: 'base',
env: {
ENVIRONMENT: 'production',
},
buildType: 'app-bundle',
},
staging: {
extends: 'base',
env: {
ENVIRONMENT: 'staging',
},
distribution: 'internal',
buildType: 'apk',
},
debug: {
extends: 'base',
withoutCredentials: true,
env: {
ENVIRONMENT: 'staging',
},
distribution: 'internal',
buildType: 'development-client',
},
},
ios: {
base: {
image: 'latest',
node: '12.13.0',
yarn: '1.22.5',
},
release: {
extends: 'base',
buildType: 'release',
env: {
ENVIRONMENT: 'production',
},
},
inhouse: {
extends: 'base',
distribution: 'internal',
enterpriseProvisioning: 'universal',
env: {
ENVIRONMENT: 'staging',
},
},
adhoc: {
extends: 'base',
distribution: 'internal',
env: {
ENVIRONMENT: 'staging',
},
},
client: {
extends: 'adhoc',
buildType: 'development-client',
},
},
},
} as DeprecatedEasJson;
expect(migrateProfile(easJson, 'base')).toEqual({
android: {
env: {
EXAMPLE_ENV: 'example value',
},
image: 'default',
},
ios: {
image: 'latest',
node: '12.13.0',
yarn: '1.22.5',
},
});
expect(migrateProfile(easJson, 'release')).toEqual({
extends: 'base',
android: {
buildType: 'app-bundle',
env: {
ENVIRONMENT: 'production',
},
},
ios: {
env: {
ENVIRONMENT: 'production',
},
},
});
expect(migrateProfile(easJson, 'staging')).toEqual({
android: {
buildType: 'apk',
distribution: 'internal',
env: {
ENVIRONMENT: 'staging',
},
},
});
expect(migrateProfile(easJson, 'inhouse')).toEqual({
ios: {
distribution: 'internal',
enterpriseProvisioning: 'universal',
env: {
ENVIRONMENT: 'staging',
},
},
});
});
test('migration for example generic eas.json', async () => {
const easJson = {
builds: {
android: {
base: {
image: 'ubuntu-18.04-android-30-ndk-r19c',
ndk: '21.4.7075529',
env: {
EXAMPLE_ENV: 'example value',
},
},
release: {
extends: 'base',
env: {
ENVIRONMENT: 'production',
},
gradleCommand: ':app:bundleRelease',
},
staging: {
extends: 'base',
env: {
ENVIRONMENT: 'staging',
},
distribution: 'internal',
gradleCommand: ':app:assembleRelease',
},
debug: {
extends: 'base',
withoutCredentials: true,
env: {
ENVIRONMENT: 'staging',
},
distribution: 'internal',
gradleCommand: ':app:assembleDebug',
},
},
ios: {
base: {
image: 'latest',
node: '12.13.0',
yarn: '1.22.5',
},
release: {
extends: 'base',
schemeBuildConfiguration: 'Release',
scheme: 'testapp',
env: {
ENVIRONMENT: 'production',
},
},
inhouse: {
extends: 'base',
distribution: 'internal',
enterpriseProvisioning: 'universal',
scheme: 'testapp-enterprise',
env: {
ENVIRONMENT: 'staging',
},
},
adhoc: {
extends: 'base',
distribution: 'internal',
scheme: 'testapp',
env: {
ENVIRONMENT: 'staging',
},
},
},
},
} as DeprecatedEasJson;
expect(migrateProfile(easJson, 'base')).toEqual({
android: {
env: {
EXAMPLE_ENV: 'example value',
},
image: 'ubuntu-18.04-android-30-ndk-r19c',
ndk: '21.4.7075529',
},
ios: {
image: 'latest',
node: '12.13.0',
yarn: '1.22.5',
},
});
expect(migrateProfile(easJson, 'release')).toEqual({
extends: 'base',
android: {
env: {
ENVIRONMENT: 'production',
},
gradleCommand: ':app:bundleRelease',
},
ios: {
buildConfiguration: 'Release',
env: {
ENVIRONMENT: 'production',
},
scheme: 'testapp',
},
});
});
+5 -6
View File
@@ -1,13 +1,12 @@
export {
CredentialsSource,
AndroidDistributionType,
AndroidBuildProfile,
BuildProfile,
IosDistributionType,
CredentialsSource,
DistributionType,
EasJson,
IosBuildProfile,
IosEnterpriseProvisioning,
DistributionType,
EasConfig,
VersionAutoIncrement,
} from './Config.types';
} from './EasJson.types';
export { EasJsonReader } from './EasJsonReader';
export { hasMismatchedExtendsAsync, isUsingDeprecatedFormatAsync, migrateAsync } from './migrate';
+172
View File
@@ -0,0 +1,172 @@
import { Android, Ios } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { AndroidBuildProfile, IosBuildProfile } from './DeprecatedConfig.types';
import { EasJson as DeprecatedEasJson, EasJsonReader } from './DeprecatedEasJsonReader';
import { EasJson, RawBuildProfile } from './EasJson.types';
export async function isUsingDeprecatedFormatAsync(projectDir: string): Promise<boolean> {
const easJsonPath = path.join(projectDir, 'eas.json');
if (!(await fs.pathExists(easJsonPath))) {
return false;
}
const rawFile = await fs.readFile(easJsonPath, 'utf8');
const json = JSON.parse(rawFile);
return !!json?.builds;
}
export async function hasMismatchedExtendsAsync(projectDir: string): Promise<boolean> {
const rawFile = await fs.readFile(path.join(projectDir, 'eas.json'), 'utf8');
const rawEasJson = JSON.parse(rawFile) as DeprecatedEasJson;
const profiles = new Set<string>();
Object.keys(rawEasJson.builds.android ?? {}).forEach(profile => profiles.add(profile));
Object.keys(rawEasJson.builds.ios ?? {}).forEach(profile => profiles.add(profile));
let hasMismatchedExtendsKeys = false;
profiles.forEach(profileName => {
if (
rawEasJson?.builds?.ios?.[profileName]?.extends !==
rawEasJson?.builds?.android?.[profileName]?.extends
) {
hasMismatchedExtendsKeys = true;
}
});
return hasMismatchedExtendsKeys;
}
export async function migrateAsync(projectDir: string): Promise<void> {
const reader = new EasJsonReader(projectDir, 'all');
try {
await reader.validateAsync();
} catch (err) {
throw new Error(`Valid eas.json is required to migrate to the new format\n${err.msg}`);
}
const rawFile = await fs.readFile(path.join(projectDir, 'eas.json'), 'utf8');
const rawEasJson = JSON.parse(rawFile) as DeprecatedEasJson;
const profiles = new Set<string>();
Object.keys(rawEasJson.builds.android ?? {}).forEach(profile => profiles.add(profile));
Object.keys(rawEasJson.builds.ios ?? {}).forEach(profile => profiles.add(profile));
const result: EasJson = {
build: {},
};
profiles.forEach(profileName => {
result.build[profileName] = migrateProfile(rawEasJson, profileName);
});
await fs.writeFile(path.join(projectDir, 'eas.json'), `${JSON.stringify(result, null, 2)}\n`);
}
interface MigrateContext {
androidProfile?: Partial<AndroidBuildProfile>;
iosProfile?: Partial<IosBuildProfile>;
}
export function migrateProfile(
rawEasJson: DeprecatedEasJson,
profileName: string
): RawBuildProfile {
const androidProfile = (rawEasJson?.builds?.android?.[profileName] ??
{}) as AndroidBuildProfile & { extends?: string };
const iosProfile = (rawEasJson?.builds?.ios?.[profileName] ?? {}) as IosBuildProfile & {
extends?: string;
};
let profile: RawBuildProfile = {
android: {},
ios: {},
};
const ctx = { androidProfile, iosProfile };
const androidCtx = { androidProfile };
const iosCtx = { iosProfile };
// simple common values
profile = migrateProperty('credentialsSource', profile, ctx);
profile = migrateProperty('releaseChannel', profile, ctx);
profile = migrateProperty('channel', profile, ctx);
profile = migrateProperty('node', profile, ctx);
profile = migrateProperty('yarn', profile, ctx);
profile = migrateProperty('expoCli', profile, ctx);
if (androidProfile.extends && androidProfile.extends === iosProfile.extends) {
profile = migrateProperty('extends', profile, ctx);
}
// android
profile = migrateProperty('image', profile, androidCtx);
profile = migrateProperty('ndk', profile, androidCtx);
profile = migrateProperty('gradleCommand', profile, androidCtx);
profile = migrateProperty('artifactPath', profile, androidCtx);
profile = migrateProperty('env', profile, androidCtx);
profile = migrateProperty('cache', profile, androidCtx);
profile = migrateProperty('withoutCredentials', profile, androidCtx);
// ios
profile = migrateProperty('enterpriseProvisioning', profile, iosCtx);
profile = migrateProperty('autoIncrement', profile, iosCtx);
profile = migrateProperty('image', profile, iosCtx);
profile = migrateProperty('bundler', profile, iosCtx);
profile = migrateProperty('fastlane', profile, iosCtx);
profile = migrateProperty('cocoapods', profile, iosCtx);
profile = migrateProperty('artifactPath', profile, iosCtx);
profile = migrateProperty('scheme', profile, iosCtx);
profile = migrateProperty('env', profile, iosCtx);
profile = migrateProperty('cache', profile, iosCtx);
profile = migrateProperty('developmentClient', profile, {
androidProfile: {
...androidProfile,
developmentClient:
androidProfile.buildType === Android.BuildType.DEVELOPMENT_CLIENT || undefined,
} as any,
iosProfile: {
...iosProfile,
developmentClient: iosProfile.buildType === Ios.BuildType.DEVELOPMENT_CLIENT || undefined,
} as any,
});
if (
androidProfile.buildType &&
androidProfile.buildType !== Android.BuildType.DEVELOPMENT_CLIENT
) {
profile.android!.buildType = androidProfile.buildType;
}
if (iosProfile.distribution === 'simulator') {
profile.ios!.simulator = true;
delete (iosProfile as any).distribution;
}
profile = migrateProperty('distribution', profile, ctx);
if (iosProfile.schemeBuildConfiguration) {
profile.ios!.buildConfiguration = iosProfile.schemeBuildConfiguration;
}
if (Object.keys(profile.android ?? {}).length === 0) {
delete profile.android;
}
if (Object.keys(profile.ios ?? {}).length === 0) {
delete profile.ios;
}
return profile;
}
function migrateProperty(
key: string,
profile: RawBuildProfile,
ctx: MigrateContext
): RawBuildProfile {
const androidProperty = (ctx.androidProfile as any)?.[key];
const iosProperty = (ctx.iosProfile as any)?.[key];
if (androidProperty && iosProperty && androidProperty === iosProperty) {
return { ...profile, [key]: androidProperty };
} else {
return {
...profile,
android: {
...profile.android,
...(androidProperty ? { [key]: androidProperty } : {}),
},
ios: {
...profile.ios,
...(iosProperty ? { [key]: iosProperty } : {}),
},
};
}
}
+230
View File
@@ -0,0 +1,230 @@
declare module 'metro-config' {
import { IncomingMessage, ServerResponse } from 'http';
import type {
DeltaResult,
Graph,
JsTransformerConfig,
Module,
Reporter,
SerializerOptions,
Server,
TransformVariants,
} from 'metro';
type CacheStore = unknown;
type CustomResolver = unknown;
import type { BasicSourceMap, MixedSourceMap } from 'metro-source-map';
//#region metro/packages/metro-config/src/configTypes.flow.js
export type PostMinifyProcess = (arg: {
code: string;
map: BasicSourceMap | null | undefined;
}) => {
code: string;
map: BasicSourceMap | null | undefined;
};
export type PostProcessBundleSourcemap = (arg: {
code: Buffer | string;
map: MixedSourceMap;
outFileName: string;
}) => {
code: Buffer | string;
map: MixedSourceMap | string;
};
interface ExtraTransformOptions {
readonly preloadedModules:
| {
[path: string]: true;
}
| false;
readonly ramGroups: string[];
readonly transform: {
readonly experimentalImportSupport: boolean;
readonly inlineRequires:
| {
readonly blacklist: {
[key: string]: true;
};
}
| boolean;
readonly nonInlinedRequires?: string[];
readonly unstable_disableES6Transforms?: boolean;
};
}
export interface GetTransformOptionsOpts {
dev: boolean;
hot: boolean;
platform: string | null | undefined;
}
export type GetTransformOptions = (
entryPoints: readonly string[],
options: GetTransformOptionsOpts,
getDependenciesOf: (path: string) => Promise<string[]>
) => Promise<ExtraTransformOptions>;
export type Middleware = (
req: IncomingMessage,
res: ServerResponse,
next: (e: Error | null | undefined) => unknown
) => unknown;
interface ResolverConfigT {
assetExts: readonly string[];
assetResolutions: readonly string[];
blacklistRE: RegExp;
dependencyExtractor: string | null | undefined;
extraNodeModules: {
[name: string]: string;
};
hasteImplModulePath: string | null | undefined;
platforms: readonly string[];
resolverMainFields: readonly string[];
resolveRequest: CustomResolver | null | undefined;
sourceExts: readonly string[];
useWatchman: boolean;
}
interface SerializerConfigT {
createModuleIdFactory: () => (path: string) => number;
customSerializer: (
entryPoint: string,
preModules: Module[],
graph: Graph,
options: SerializerOptions
) => string | { code: string; map: string } | null | undefined;
experimentalSerializerHook: (graph: Graph, delta: DeltaResult) => unknown;
getModulesRunBeforeMainModule: (entryFilePath: string) => string[];
getPolyfills: (arg: { platform: string | null | undefined }) => readonly string[];
getRunModuleStatement: (moduleId: number | string) => string;
polyfillModuleNames: readonly string[];
postProcessBundleSourcemap: PostProcessBundleSourcemap;
processModuleFilter: (modules: Module) => boolean;
}
type TransformerConfigT = JsTransformerConfig & {
getTransformOptions: GetTransformOptions;
postMinifyProcess: PostMinifyProcess;
transformVariants: TransformVariants;
workerPath: string;
publicPath: string;
experimentalImportBundleSupport: false;
};
interface MetalConfigT {
cacheStores: readonly any[];
cacheVersion: string;
hasteMapCacheDirectory?: string;
maxWorkers: number;
projectRoot: string;
stickyWorkers: boolean;
transformerPath: string;
reporter: Reporter;
resetCache: boolean;
watchFolders: readonly string[];
}
interface ServerConfigT {
enhanceMiddleware: (middleware: Middleware, server: Server) => Middleware;
useGlobalHotkey: boolean;
port: number;
runInspectorProxy: boolean;
verifyConnections: boolean;
}
type StackFrameCustomizations = undefined | { collapse?: boolean };
interface SymbolicatorConfigT {
customizeFrame: (
frame: Readonly<{
file: string | null;
lineNumber: number | null;
column: number | null;
methodName: string | null;
}>
) => StackFrameCustomizations | Promise<StackFrameCustomizations>;
}
export type InputConfigT = Partial<
MetalConfigT &
Readonly<{
resolver: Partial<ResolverConfigT>;
server: Partial<ServerConfigT>;
serializer: Partial<SerializerConfigT>;
symbolicator: Partial<SymbolicatorConfigT>;
transformer: Partial<TransformerConfigT>;
}>
>;
export type IntermediateConfigT = MetalConfigT & {
resolver: ResolverConfigT;
server: ServerConfigT;
serializer: SerializerConfigT;
symbolicator: SymbolicatorConfigT;
transformer: TransformerConfigT;
};
export type ConfigT = Readonly<
Readonly<MetalConfigT> &
Readonly<{
resolver: Readonly<ResolverConfigT>;
server: Readonly<ServerConfigT>;
serializer: Readonly<SerializerConfigT>;
symbolicator: Readonly<SymbolicatorConfigT>;
transformer: Readonly<TransformerConfigT>;
}>
>;
//#endregion
//#region metro/packages/metro-config/src/index.js
interface CosmiConfigResult {
filepath: string;
isEmpty: boolean;
config: ((arg: ConfigT) => Promise<ConfigT>) | ((arg: ConfigT) => ConfigT) | InputConfigT;
}
interface YargArguments {
config?: string;
cwd?: string;
port?: string | number;
host?: string;
projectRoot?: string;
watchFolders?: string[];
assetExts?: string[];
sourceExts?: string[];
platforms?: string[];
'max-workers'?: string | number;
maxWorkers?: string | number;
transformer?: string;
'reset-cache'?: boolean;
resetCache?: boolean;
runInspectorProxy?: boolean;
verbose?: boolean;
}
export function resolveConfig(path?: string, cwd?: string): Promise<CosmiConfigResult>;
export function mergeConfig<T extends InputConfigT>(
defaultConfig: T,
...configs: InputConfigT[]
): T;
export function loadConfig(
argv: YargArguments,
defaultConfigOverrides: InputConfigT
): Promise<ConfigT>;
export function getDefaultConfig(rootPath?: string): Promise<ConfigT>;
namespace getDefaultConfig {
function getDefaultValues(rootPath?: string): ConfigT;
}
//#endregion
}
+63
View File
@@ -0,0 +1,63 @@
declare module 'metro-source-map' {
//#region metro-source-map/src/source-map.js.flow
type GeneratedCodeMapping = [number, number];
type SourceMapping = [number, number, number, number];
type SourceMappingWithName = [number, number, number, number, string];
type HermesFunctionOffsets = { [key: number]: Readonly<number[]> };
type FBSourcesArray = Readonly<(FBSourceMetadata | null)[]>;
type FBSourceMetadata = [FBSourceFunctionMap | null];
type FBSourceFunctionMap = {
names: Readonly<string[]>;
mappings: string;
};
type FBSegmentMap = { [id: string]: MixedSourceMap };
interface BasicSourceMap {
file?: string;
mappings: string;
names: string[];
sourceRoot?: string;
sources: string[];
sourcesContent?: (string | null)[];
version: number;
x_facebook_offsets?: number[];
x_metro_module_paths?: string[];
x_facebook_sources?: FBSourcesArray;
x_facebook_segments?: FBSegmentMap;
x_hermes_function_offsets?: HermesFunctionOffsets;
}
interface IndexMapSection {
map: IndexMap | BasicSourceMap;
offset: {
line: number;
column: number;
};
}
interface IndexMap {
file?: string;
mappings?: void; // avoids SourceMap being a disjoint union
sections: IndexMapSection[];
version: number;
x_facebook_offsets?: number[];
x_metro_module_paths?: string[];
x_facebook_sources?: FBSourcesArray;
x_facebook_segments?: FBSegmentMap;
x_hermes_function_offsets?: HermesFunctionOffsets;
}
type MixedSourceMap = IndexMap | BasicSourceMap;
//#endregion
//#region metro-source-map/src/composeSourceMaps.js.flow
export function composeSourceMaps(maps: Readonly<MixedSourceMap[]>): MixedSourceMap;
//#endregion
}
+486
View File
@@ -0,0 +1,486 @@
declare module 'metro' {
//#region metro/src/Assets.js
type AssetDataWithoutFiles = {
readonly __packager_asset: true;
readonly fileSystemLocation: string;
readonly hash: string;
readonly height: number | null | undefined;
readonly httpServerLocation: string;
readonly name: string;
readonly scales: number[];
readonly type: string;
readonly width: number | null | undefined;
};
export type AssetData = AssetDataWithoutFiles & { readonly files: string[] };
//#endregion
//#region metro/src/DeltaBundler/types.flow.js
type DynamicRequiresBehavior = unknown;
type MinifierConfig = unknown;
type BundleDetails = unknown;
type GlobalCacheDisabledReason = unknown;
type MixedSourceMap = unknown;
type MetroSourceMapSegmentTuple = unknown;
interface MixedOutput {
readonly data: unknown;
readonly type: string;
}
interface BabelSourceLocation {
start: { line: number; column: number };
end: { line: number; column: number };
identifierName?: string;
}
interface TransformResultDependency {
/**
* The literal name provided to a require or import call. For example 'foo' in
* case of `require('foo')`.
*/
readonly name: string;
/**
* Extra data returned by the dependency extractor. Whatever is added here is
* blindly piped by Metro to the serializers.
*/
readonly data: {
/**
* If `true` this dependency is due to a dynamic `import()` call. If `false`,
* this dependency was pulled using a synchronous `require()` call.
*/
readonly isAsync: boolean;
/**
* The dependency is actually a `__prefetchImport()` call.
*/
readonly isPrefetchOnly?: true;
/**
* The condition for splitting on this dependency edge.
*/
readonly splitCondition?: {
readonly mobileConfigName: string;
};
/**
* The dependency is enclosed in a try/catch block.
*/
readonly isOptional?: boolean;
readonly locs: readonly BabelSourceLocation[];
};
}
interface Dependency {
readonly absolutePath: string;
readonly data: TransformResultDependency;
}
export interface Module<T = MixedOutput> {
readonly dependencies: Map<string, Dependency>;
readonly inverseDependencies: Set<string>;
readonly output: readonly T[];
readonly path: string;
readonly getSource: () => Buffer;
}
export interface Graph<T = MixedOutput> {
dependencies: Map<string, Module<T>>;
importBundleNames: Set<string>;
readonly entryPoints: readonly string[];
}
export type TransformResult<T = MixedOutput> = Readonly<{
dependencies: readonly TransformResultDependency[];
output: readonly T[];
}>;
interface AllowOptionalDependenciesWithOptions {
readonly exclude: string[];
}
type AllowOptionalDependencies = boolean | AllowOptionalDependenciesWithOptions;
export interface DeltaResult<T = MixedOutput> {
readonly added: Map<string, Module<T>>;
readonly modified: Map<string, Module<T>>;
readonly deleted: Set<string>;
readonly reset: boolean;
}
export interface SerializerOptions {
readonly asyncRequireModulePath: string;
readonly createModuleId: (arg0: string) => number;
readonly dev: boolean;
readonly getRunModuleStatement: (arg0: number | string) => string;
readonly inlineSourceMap: boolean | null | undefined;
readonly modulesOnly: boolean;
readonly processModuleFilter: (module: Module) => boolean;
readonly projectRoot: string;
readonly runBeforeMainModule: readonly string[];
readonly runModule: boolean;
readonly sourceMapUrl: string | null | undefined;
readonly sourceUrl: string | null | undefined;
}
//#endregion
//#region metro/src/DeltaBundler/Serializers/getRamBundleInfo.js
interface RamBundleInfo {
getDependencies: (filePath: string) => Set<string>;
startupModules: readonly ModuleTransportLike[];
lazyModules: readonly ModuleTransportLike[];
groups: Map<number, Set<number>>;
}
//#endregion
//#region metro/src/index.js
import { Server as HttpServer, IncomingMessage, ServerResponse } from 'http';
import { Server as HttpsServer } from 'https';
import { ConfigT, InputConfigT, Middleware, loadConfig } from 'metro-config';
type MetroMiddleWare = {
attachHmrServer: (httpServer: HttpServer | HttpsServer) => void;
end: () => void;
metroServer: Server;
middleware: Middleware;
};
type RunServerOptions = {
hasReducedPerformance?: boolean;
hmrEnabled?: boolean;
host?: string;
onError?: (arg0: Error & { code?: string }) => void;
onReady?: (server: HttpServer | HttpsServer) => void;
runInspectorProxy?: boolean;
secure?: boolean;
secureCert?: string;
secureKey?: string;
};
type BuildGraphOptions = {
entries: readonly string[];
customTransformOptions?: CustomTransformOptions;
dev?: boolean;
minify?: boolean;
onProgress?: (transformedFileCount: number, totalFileCount: number) => void;
platform?: string;
type?: 'module' | 'script';
};
type RunBuildOptions = {
entry: string;
dev?: boolean;
out?: string;
onBegin?: () => void;
onComplete?: () => void;
onProgress?: (transformedFileCount: number, totalFileCount: number) => void;
minify?: boolean;
output?: {
build: (
arg0: Server,
arg1: RequestOptions
) => Promise<{
code: string;
map: string;
}>;
save: (
arg0: {
code: string;
map: string;
},
arg1: OutputOptions,
arg2: (...args: string[]) => void
) => Promise<unknown>;
};
platform?: string;
sourceMap?: boolean;
sourceMapUrl?: string;
};
export function runMetro(config: InputConfigT, options?: ServerOptions): Promise<Server>;
export { loadConfig };
export function createConnectMiddleware(
config: ConfigT,
options?: ServerOptions
): Promise<MetroMiddleWare>;
export function runServer(
config: ConfigT,
options: RunServerOptions
): Promise<HttpServer | HttpsServer>;
export function runBuild(
config: ConfigT,
options: RunBuildOptions
): Promise<{
code: string;
map: string;
}>;
export function buildGraph(config: InputConfigT, options: BuildGraphOptions): Promise<Graph>;
//#endregion
//#region metro/src/JSTransformer/worker.js
type CustomTransformOptions = {
[key: string]: unknown;
};
export type JsTransformerConfig = Readonly<{
assetPlugins: readonly string[];
assetRegistryPath: string;
asyncRequireModulePath: string;
babelTransformerPath: string;
dynamicDepsInPackages: DynamicRequiresBehavior;
enableBabelRCLookup: boolean;
enableBabelRuntime: boolean;
experimentalImportBundleSupport: boolean;
minifierConfig: MinifierConfig;
minifierPath: string;
optimizationSizeLimit: number;
publicPath: string;
allowOptionalDependencies: AllowOptionalDependencies;
}>;
//#endregion
//#region metro/src/lib/reporting.js
/**
* A tagged union of all the actions that may happen and we may want to
* report to the tool user.
*/
export type ReportableEvent =
| {
port: number;
hasReducedPerformance: boolean;
type: 'initialize_started';
}
| {
type: 'initialize_failed';
port: number;
error: Error;
}
| {
buildID: string;
type: 'bundle_build_done';
}
| {
buildID: string;
type: 'bundle_build_failed';
}
| {
buildID: string;
bundleDetails: BundleDetails;
type: 'bundle_build_started';
}
| {
error: Error;
type: 'bundling_error';
}
| {
type: 'dep_graph_loading';
hasReducedPerformance: boolean;
}
| { type: 'dep_graph_loaded' }
| {
buildID: string;
type: 'bundle_transform_progressed';
transformedFileCount: number;
totalFileCount: number;
}
| {
type: 'global_cache_error';
error: Error;
}
| {
type: 'global_cache_disabled';
reason: GlobalCacheDisabledReason;
}
| { type: 'transform_cache_reset' }
| {
type: 'worker_stdout_chunk';
chunk: string;
}
| {
type: 'worker_stderr_chunk';
chunk: string;
}
| {
type: 'hmr_client_error';
error: Error;
}
| {
type: 'client_log';
level:
| 'trace'
| 'info'
| 'warn'
| 'log'
| 'group'
| 'groupCollapsed'
| 'groupEnd'
| 'debug';
data: unknown[];
};
/**
* Code across the application takes a reporter as an option and calls the
* update whenever one of the ReportableEvent happens. Code does not directly
* write to the standard output, because a build would be:
*
* 1. ad-hoc, embedded into another tool, in which case we do not want to
* pollute that tool's own output. The tool is free to present the
* warnings/progress we generate any way they want, by specifing a custom
* reporter.
* 2. run as a background process from another tool, in which case we want
* to expose updates in a way that is easily machine-readable, for example
* a JSON-stream. We don't want to pollute it with textual messages.
*
* We centralize terminal reporting into a single place because we want the
* output to be robust and consistent. The most common reporter is
* TerminalReporter, that should be the only place in the application should
* access the `terminal` module (nor the `console`).
*/
export interface Reporter {
update(event: ReportableEvent): void;
}
//#endregion
//#region metro/src/ModuleGraph/types.flow.js
export type TransformVariants = {
readonly [name: string]: object;
};
//#endregion
//#region metro/src/Server.js
type ServerOptions = Readonly<{
watch?: boolean;
}>;
//#endregion
//#region metro/src/Server/index.js
type IncrementalBundler = unknown;
export class Server {
constructor(config: ConfigT, options?: ServerOptions);
end(): void;
getBundler(): IncrementalBundler;
getCreateModuleId(): (path: string) => number;
build(options: BundleOptions): Promise<{
code: string;
map: string;
}>;
getRamBundleInfo(options: BundleOptions): Promise<RamBundleInfo>;
getAssets(options: BundleOptions): Promise<readonly AssetData[]>;
getOrderedDependencyPaths(options: {
readonly dev: boolean;
readonly entryFile: string;
readonly minify: boolean;
readonly platform: string;
}): Promise<string[]>;
processRequest(
req: IncomingMessage,
res: ServerResponse,
next: (arg0: Error | null | undefined) => unknown
): void;
getNewBuildID(): string;
getPlatforms(): readonly string[];
getWatchFolders(): readonly string[];
static DEFAULT_GRAPH_OPTIONS: {
customTransformOptions: any;
dev: boolean;
hot: boolean;
minify: boolean;
};
static DEFAULT_BUNDLE_OPTIONS: typeof Server.DEFAULT_GRAPH_OPTIONS & {
excludeSource: false;
inlineSourceMap: false;
modulesOnly: false;
onProgress: null;
runModule: true;
shallow: false;
sourceMapUrl: null;
sourceUrl: null;
};
}
//#endregion
//#region metro/src/shared/types.flow.js
type BundleType = 'bundle' | 'delta' | 'meta' | 'map' | 'ram' | 'cli' | 'hmr' | 'todo' | 'graph';
type MetroSourceMapOrMappings = MixedSourceMap | MetroSourceMapSegmentTuple[];
export interface BundleOptions {
bundleType: BundleType;
customTransformOptions: CustomTransformOptions;
dev: boolean;
entryFile: string;
readonly excludeSource: boolean;
readonly hot: boolean;
readonly inlineSourceMap: boolean;
minify: boolean;
readonly modulesOnly: boolean;
onProgress: (doneCont: number, totalCount: number) => unknown | null | undefined;
readonly platform: string | null | undefined;
readonly runModule: boolean;
readonly shallow: boolean;
sourceMapUrl: string | null | undefined;
sourceUrl: string | null | undefined;
createModuleIdFactory?: () => (path: string) => number;
}
type ModuleTransportLike = {
readonly code: string;
readonly id: number;
readonly map: MetroSourceMapOrMappings | null | undefined;
readonly name?: string;
readonly sourcePath: string;
};
export interface OutputOptions {
bundleOutput: string;
bundleEncoding?: 'utf8' | 'utf16le' | 'ascii';
dev?: boolean;
indexedRamBundle?: boolean;
platform: string;
sourcemapOutput?: string;
sourcemapSourcesRoot?: string;
sourcemapUseAbsolutePath?: boolean;
}
export interface RequestOptions {
entryFile: string;
inlineSourceMap?: boolean;
sourceMapUrl?: string;
dev?: boolean;
minify: boolean;
platform: string;
createModuleIdFactory?: () => (path: string) => number;
onProgress?: (transformedFileCount: number, totalFileCount: number) => void;
}
//#endregion
}
+1 -3
View File
@@ -407,9 +407,7 @@ declare module 'xcode' {
*/
findPBXGroupKey(criteria: { name?: string; path?: string }): UUID | undefined;
findPBXVariantGroupKey(criteria: unknown): string;
addLocalizationVariantGroup(
name: unknown
): {
addLocalizationVariantGroup(name: unknown): {
uuid: unknown;
fileRef: unknown;
basename: unknown;
Generated
+712 -449
View File
File diff suppressed because it is too large. Load diff