Compare commits

...
14 Commits
Author SHA1 Message Date
Wojciech Kozyra 2d117e1d7e v0.22.3 2021-08-02 12:49:51 +02:00
Sallar 986674942d [eas-json][eas-cli] add simulator key to build profile json validator (#546)
* Add simulator to build profile json validator

* Update CHANGELOG.md

* Fix PR number

* Change the order of simulator key
2021-08-02 12:46:21 +02:00
Wojciech Kozyra 13c224589b Update CHANGELOG 2021-08-02 10:57:16 +02:00
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
55 changed files with 3102 additions and 1151 deletions

No files matched your search

+27
View File
@@ -8,6 +8,33 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Fix build profile schema validation when `simulator` key is present for iOS ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
### 🧹 Chores
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
### 🛠 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))
## [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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.22.1"
"version": "0.22.3"
}
+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.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.3/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.1/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.22.3/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.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.22.3/packages/eas-cli/src/commands/webhook/view.ts)_
<!-- commandsstop -->
+522 -328
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
@@ -5647,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",
@@ -6788,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",
@@ -9141,223 +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
},
{
"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": "OBJECT",
"name": "EnvironmentSecret",
@@ -21728,81 +21997,6 @@
"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": "ENUM",
"name": "CacheControlScope",
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.22.1",
"version": "0.22.3",
"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.1",
"@expo/eas-build-job": "0.2.45",
"@expo/eas-json": "^0.22.3",
"@expo/json-file": "8.2.25",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.11",
@@ -60,6 +60,7 @@ This means that it will most likely produce an AAB and you will not be able to i
await validateAndSyncProjectConfigurationAsync(ctx.projectDir, ctx.exp);
}
},
getMetadataContext: () => ({}),
prepareJobAsync,
sendBuildRequestAsync: async (
appId: string,
@@ -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,11 +14,18 @@ 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.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const projectRootDirectory = path.relative(await vcs.getRootPathAsync(), ctx.projectDir) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
@@ -33,27 +41,27 @@ 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,
...cacheDefaults,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
@@ -62,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);
+17 -20
View File
@@ -18,7 +18,7 @@ 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';
@@ -41,6 +41,7 @@ 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>;
}
@@ -52,54 +53,50 @@ export async function prepareBuildRequestForPlatformAsync<
Credentials,
TJob extends Job
>(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.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.nonInteractive,
}
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
const projectArchive = builder.ctx.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.local) {
if (ctx.local) {
await runLocalBuildAsync(job);
return undefined;
} else {
@@ -354,7 +351,7 @@ export async function waitForBuildEndAsync(
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;
const unknownState = builds.length - newBuilds - inQueue - inProgress - errored - finished;
spinner.text = [
newBuilds && `Builds created: ${newBuilds}`,
inQueue && `Builds in queue: ${inQueue}`,
+22 -106
View File
@@ -26,24 +26,16 @@ export async function ensureProjectConfiguredAsync(
projectDir: string,
requestedPlatform: RequestedPlatform
): Promise<void> {
const platformToConfigure = await getPlatformToConfigureAsync(projectDir, requestedPlatform);
if (!platformToConfigure) {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
return;
}
// 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 (platformToConfigure === RequestedPlatform.Ios) {
message = 'Your iOS project is not configured to build with EAS. Set it up now?';
} else if (platformToConfigure === RequestedPlatform.Android) {
message = 'Your Android project is not configured to build with EAS. Set it up now?';
}
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: platformToConfigure,
platform: requestedPlatform,
});
if (await vcs.hasUncommittedChangesAsync()) {
error(
@@ -103,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 = EasJsonReader.formatEasJsonPath(ctx.projectDir);
let existingEasJson;
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 {
@@ -228,30 +171,3 @@ async function reviewAndCommitChangesAsync(
await reviewAndCommitChangesAsync(initialCommitMessage, false);
}
}
async function getPlatformToConfigureAsync(
projectDir: string,
platform: RequestedPlatform
): Promise<RequestedPlatform | null> {
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
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;
}
} else if (
(platform === RequestedPlatform.Android || platform === RequestedPlatform.Ios) &&
easConfig.builds?.[platform]
) {
return null;
}
return platform;
}
+10 -18
View File
@@ -1,6 +1,6 @@
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';
@@ -13,6 +13,7 @@ import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { Platform, RequestedPlatform, TrackingContext } from './types';
import Analytics, { Event } from './utils/analytics';
export interface ConfigureContext {
user: Actor;
projectDir: string;
@@ -24,13 +25,9 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
? AndroidBuildProfile
: IosBuildProfile;
export interface BuildContext<T extends Platform> {
accountName: string;
buildProfile: PlatformBuildProfile<T>;
buildProfile: BuildProfile<T>;
buildProfileName: string;
clearCache: boolean;
exp: ExpoConfig;
@@ -48,8 +45,8 @@ export interface BuildContext<T extends Platform> {
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
easConfig,
local,
nonInteractive = false,
platform,
@@ -57,20 +54,15 @@ export async function createBuildContextAsync<T extends Platform>({
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
easConfig: EasConfig;
local: boolean;
nonInteractive: boolean;
platform: T;
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, buildProfile.env);
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
@@ -126,10 +118,10 @@ function getDevClientEventProperties({
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;
}
@@ -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>`,
},
+16 -1
View File
@@ -1,13 +1,15 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Workflow } from '@expo/eas-build-job';
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 { 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';
@@ -51,6 +53,19 @@ export async function prepareIosBuildAsync(
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)) {
@@ -41,5 +41,5 @@ export async function ensureIosCredentialsAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return buildCtx.buildProfile.distribution !== 'simulator';
return !buildCtx.buildProfile.simulator;
}
+13 -5
View File
@@ -15,6 +15,12 @@ interface JobData {
buildScheme: string;
}
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
@@ -29,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,
@@ -46,6 +52,7 @@ export async function prepareJobAsync(
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
clear: ctx.clearCache,
},
@@ -56,13 +63,14 @@ 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);
}
+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);
}
+24 -14
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,22 +28,26 @@ 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,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
...channelOrReleaseChannel,
@@ -58,23 +63,28 @@ export async function collectMetadata<T extends Platform>(
),
}),
};
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.projectDir, ctx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readShortVersionAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
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.projectDir, ctx.exp);
const iosContext = platformContext as IosMetadataContext | undefined;
return await readBuildNumberAsync(ctx.projectDir, ctx.exp, iosContext?.buildSettings ?? {});
} else {
const versionCode = await readVersionCodeAsync(ctx.projectDir, ctx.exp);
return versionCode !== undefined ? String(versionCode) : 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>;
@@ -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);
}
@@ -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];
+58 -8
View File
@@ -1,4 +1,10 @@
import { EasJsonReader } from '@expo/eas-json';
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';
@@ -17,8 +23,9 @@ import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { confirmAsync, promptAsync } from '../../prompts';
import vcs from '../../vcs';
interface RawBuildFlags {
@@ -80,27 +87,27 @@ export default class Build extends EasCommand {
async run(): Promise<void> {
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);
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const easConfig = await new EasJsonReader(projectDir, requestedPlatform).readAsync(
flags.profile
);
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,
easConfig,
buildProfile: await easJsonReader.readBuildProfileAsync(flags.profile, platform),
local: flags.local,
nonInteractive: flags.nonInteractive,
platform,
@@ -108,6 +115,11 @@ export default class Build extends EasCommand {
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 });
}
@@ -210,3 +222,41 @@ export default class Build extends EasCommand {
}
}
}
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 });
}
}
}
+53 -6
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,18 +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 { flags } = this.parse(BuildList);
const {
platform: requestedPlatform,
status: buildStatus,
distribution: buildDistribution,
limit = 10,
} = this.parse(BuildList).flags;
} = 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 });
@@ -54,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) {
@@ -92,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) {
@@ -109,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;
}
};
+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));
}
}
@@ -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;
}
@@ -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;
}
@@ -11,6 +11,7 @@ export interface App {
export interface Target {
targetName: string;
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
}
@@ -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';
@@ -284,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,
@@ -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,7 +7,7 @@ import { SelectIosDistributionTypeGraphqlFromBuildProfile } from '../SelectIosDi
describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('errors with a simulator distribution', async () => {
const buildProfile = {
distribution: 'simulator',
simulator: true,
} as IosBuildProfile;
const ctx = createCtxMock({
+68 -44
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 */
@@ -835,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'
@@ -985,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']>;
};
@@ -1240,29 +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>;
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 EnvironmentSecret = {
__typename?: 'EnvironmentSecret';
id: Scalars['ID'];
@@ -3424,16 +3459,6 @@ export type DeleteWebhookResult = {
id: Scalars['ID'];
};
/** 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 enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
@@ -4868,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>;
}>;
@@ -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()
);
+8 -2
View File
@@ -1,15 +1,21 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
export function getExpoConfig(projectDir: string, env: Env = {}): ExpoConfig {
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,
...env,
...opts.env,
};
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
});
return exp;
} finally {
+1 -1
View File
@@ -34,7 +34,7 @@ export async function resolveXcodeBuildContextAsync(
return {
buildScheme,
buildConfiguration:
buildProfile.schemeBuildConfiguration ??
buildProfile.buildConfiguration ??
(await IOSConfig.BuildScheme.getArchiveBuildConfigurationForSchemeAsync(
projectDir,
buildScheme
@@ -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,
@@ -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 {}
+1 -1
View File
@@ -61,7 +61,7 @@ export async function setProjectIdAsync(
projectDir: string,
options: { env?: Env } = {}
): Promise<ExpoConfig | undefined> {
const exp = getExpoConfig(projectDir, options.env);
const exp = getExpoConfig(projectDir, options);
const privacy = toAppPrivacy(exp.privacy);
const projectId = await ensureProjectExistsAsync({
@@ -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) {
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.22.1",
"version": "0.22.3",
"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": "2.3.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;
};
}
+75 -132
View File
@@ -1,107 +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 {
public static formatEasJsonPath(projectDir: string) {
return path.join(projectDir, 'eas.json');
}
constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
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> {
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,
});
@@ -111,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;
}
+57 -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,73 @@ 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')
),
simulator: Joi.boolean(),
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
}
Generated
+4 -4
View File
@@ -1956,10 +1956,10 @@
xcode "^3.0.0"
xml-js "^1.6.11"
"@expo/[email protected]4":
version "0.2.44"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.44.tgz#11f442cf9c1ac8402afbb06742bc99d4c1c6b6d9"
integrity sha512-4/wtOyAFLa0aILfco0a/slL96CBUppSeXfPdLzcHkBIc+AUB/pUSGf/M+Z3Pn7QoA3BHCnSIW8DosqqBBOppRg==
"@expo/[email protected]5":
version "0.2.45"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.45.tgz#1404767e815b08ebdce8af073dd0f0e6911af1a5"
integrity sha512-ysidetne3cqg6H1zsFUCJhfHwDZEGbUhZKQk12a2NtC3P8QYALkOzwc+JbqhPX6oQd9zm9WqF1w7rc7tPK6plA==
dependencies:
"@hapi/joi" "^17.1.1"