Compare commits

...
42 Commits
Author SHA1 Message Date
Brent Vatne 11c194a168 v0.38.2 2021-11-26 13:50:29 -08:00
Brent Vatne 3c74c111a7 Update yarn.lock 2021-11-26 13:41:40 -08:00
Brent Vatne b69b8b87a7 [eas-cli] upgrade @expo/* deps (#801) 2021-11-26 13:40:58 -08:00
jkh 825baa9f68 [eas-cli] check EAS Update URL is correctly configured before publish (#789) 2021-11-26 16:23:14 -05:00
Dominik Sokal 9f5736cfaa fix lint error 2021-11-26 14:39:10 +01:00
Dominik Sokal ae24ffbe86 fix getProfilesAsync tests 2021-11-26 14:23:08 +01:00
Dominik Sokal 487316370b refactor @expo/eas-json (#794)
* refactor @expo/eas-json

* update CHANGELOG.md
2021-11-26 12:35:49 +01:00
Dominik Sokal 8bc3565270 fix evaluating template strings from info.plist (#798)
* fix evaluating template strings from info.plist

* update CHANGELOG.md
2021-11-26 11:41:57 +01:00
Expo CI 07450f6bf7 update CHANGELOG.md 2021-11-25 09:34:42 +00:00
Dominik Sokal 71d48c3cf2 v0.38.1 2021-11-25 10:29:10 +01:00
Dominik Sokal 7343e81065 upgrade @expo/* deps (#792)
* upgrade @expo/* deps

* update CHANGELOG.md

* fix import
2021-11-25 10:25:05 +01:00
Expo CI 1f1310bef6 update CHANGELOG.md 2021-11-24 13:56:49 +00:00
Dominik Sokal 27c69ec8e1 v0.38.0 2021-11-24 14:50:06 +01:00
Wojciech Kozyra b81b896bc6 [eas-cli] pass identifier from build to submit (#780)
* pass identifier from build to submit

* update CHANGELOG.md

* review feedback
2021-11-24 14:02:25 +01:00
Dominik Sokal ef79db2ca5 clean up .gitignore (#788) 2021-11-24 13:53:19 +01:00
Dominik Sokal 88565eec62 implement submit webhooks (#777)
* implement submit webhooks

* update CHANGELOG.md
2021-11-24 10:43:25 +01:00
Dominik SokalandBrent Vatne e8a520e55f better error message when git is not installed (#784)
* better error message when git is not installed

* update CHANGELOG.md

* Update packages/eas-cli/src/vcs/clients/git.ts

Co-authored-by: Brent Vatne <[email protected]>

* Update packages/eas-cli/src/vcs/clients/git.ts

Co-authored-by: Brent Vatne <[email protected]>
2021-11-24 10:42:17 +01:00
Dominik Sokal 9a33f7bd93 check npm version when building locally (#787)
* check npm version when building locally

* update CHANGELOG.md
2021-11-24 10:16:54 +01:00
kgc00 23c70e237b bump rudder node sdk (#786) 2021-11-24 09:23:16 +01:00
jkh 7fcf41db78 [eas-cli] fix bug that required runtime version for all builds (#785) 2021-11-23 15:25:44 -05:00
Evan BaconandDominik Sokal 36f4a4bd38 Improve unknown error message from #740 (#775)
* Improve unknown error message from #740

* Update CHANGELOG.md

* Update bundleIdCapabilities.ts

* Update packages/eas-cli/src/credentials/ios/appstore/bundleIdCapabilities.ts

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-23 13:22:12 -07:00
jkh 87541db80d [eas-cli] compute the runtime version for a build's metadata (#783) 2021-11-23 09:36:37 -05:00
Dominik Sokal 80112c682f upgrade typescript to 4.5.2 (#781)
* upgrade typescript to 4.5.2

* bump oclif deps

* update CHANGELOG.md
2021-11-22 16:30:02 +01:00
Expo CI eb29d3dadb update CHANGELOG.md 2021-11-18 09:51:37 +00:00
Dominik Sokal 558c367038 v0.37.0 2021-11-18 10:42:38 +01:00
Dominik Sokal 4b88d00379 upgrade eas-cli-local-build-plugin to 0.0.50 2021-11-18 10:42:16 +01:00
Dominik Sokal aad3b5ccd5 interactively configure git user.name and user.email (#772)
* interactively configure git user.name and user.email

* update CHANGELOG.md

* address PR feedback
2021-11-18 10:30:51 +01:00
Quinlan Jung 8b50806093 [eas-cli] make the missing eas.json messages better (#770)
* [eas-cli] make the missing eas.json messages better

* make error upstream

* revert

* fix tests and stuff

* update CHANGELOG.md
2021-11-17 13:39:44 -08:00
Dominik Sokal 45c9bda2e9 remove eas availability checks (#771) 2021-11-17 13:26:36 +01:00
Dominik Sokal 23237210d1 Update CHANGELOG.md 2021-11-17 09:58:36 +01:00
Dominik Sokal e34382e6a3 show plist path when parsing failed (#768)
* show plist path when parsing failed

* Update packages/eas-cli/src/build/ios/version.ts
2021-11-17 09:57:59 +01:00
jkh b8ad2ee29a [eas-cli] auto create channel on publish (#766) 2021-11-16 13:23:39 -05:00
Wojciech KozyraandDominik Sokal 6bd62bac64 [eas-cli] read correct identifiers for multi-scheme/multi-flavor projects (#765)
* [eas-cli] read correct appIdentifier for multi-scheme/multi-flavor project

* review feedback

* Update CHANGELOG.md

* Update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-16 15:52:39 +01:00
Dominik Sokal 8b959fc6d8 make the missing build/submit profile error message more descriptive (#761) 2021-11-15 20:04:34 +01:00
Bartosz Kaszubowski 6e615e20d6 [workflows] update setup-node action to v2 (#763) 2021-11-15 19:59:29 +01:00
Bartosz Kaszubowski efbc5fc4bc [workspace] update Prettier (#762) 2021-11-15 19:59:12 +01:00
Dominik Sokal 79c77ae835 validate release channel in eas.json (#764)
* validate release channel in eas.json

* update CHANGELOG.md
2021-11-15 16:52:12 +01:00
Expo CI 618b6b1caf update CHANGELOG.md 2021-11-15 11:50:31 +00:00
Dominik Sokal 864658df80 v0.36.1 2021-11-15 12:26:39 +01:00
Dominik Sokal 4510e86b64 update CHANGELOG.md 2021-11-15 12:24:53 +01:00
Dominik Sokal 4af4490abc upgrade eas-cli-local-build-plugin to 0.0.48 2021-11-15 12:23:18 +01:00
Expo CI 37c4040203 update CHANGELOG.md 2021-11-15 10:48:35 +00:00
96 changed files with 1714 additions and 1236 deletions

No files matched your search

-1
View File
@@ -11,7 +11,6 @@ module.exports = {
ignoreDeclarationSort: true,
},
],
'flowtype/no-types-missing-file-annotation': 'off',
curly: 'warn',
'import/no-cycle': 'error',
'import/no-extraneous-dependencies': [
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Checkout Pull Request
+5 -5
View File
@@ -25,7 +25,7 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
@@ -58,7 +58,7 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
@@ -81,7 +81,7 @@ jobs:
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: yarn install --frozen-lockfile --check-files
@@ -119,7 +119,7 @@ jobs:
needs: [build-linux, build-mac, build-windows]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
@@ -138,7 +138,7 @@ jobs:
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: preactjs/compressed-size-action@v2
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
+14 -10
View File
@@ -1,14 +1,18 @@
# Build directories
packages/*/build
packages/eas-cli/dist
# Code editors
.idea
.history
# macOS
.DS_Store
# Node
node_modules
*-debug.log
*-error.log
**/build/**
!packages/eas-cli/src/commands/build/**
!packages/eas-cli/src/build/**
dist
/package-lock.json
tmp
node_modules
# Tests
coverage
.history
.DS_Store
+1 -1
View File
@@ -2,7 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"jsxBracketSameLine": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
+67 -1
View File
@@ -6,11 +6,77 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- Add support for extending submit profiles. ([#794](https://github.com/expo/eas-cli/pull/794) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix reading app version and app build version of iOS projects. ([#798](https://github.com/expo/eas-cli/pull/798) by [@dsokal](https://github.com/dsokal))
- Fix push notifications entitlements. ([#801](https://github.com/expo/eas-cli/pull/801) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
### 🐛 Bug fixes
- Fix building iOS projects with whitespace in the project name. ([#792](https://github.com/expo/eas-cli/pull/792) by [@dsokal](https://github.com/dsokal))
## [0.38.0](https://github.com/expo/eas-cli/releases/tag/v0.38.0) - 2021-11-24
### 🎉 New features
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Compute runtime version policies. ([#783](https://github.com/expo/eas-cli/pull/783), [#785](https://github.com/expo/eas-cli/pull/785) by [@jkhales](https://github.com/jkhales))
- Fix local builds with npm < 7. ([#787](https://github.com/expo/eas-cli/pull/787) by [@dsokal](https://github.com/dsokal))
- Override `applicationId`/`bundleIdentifier` when auto-submitting after build. ([#780](https://github.com/expo/eas-cli/pull/780) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Upgrade `typescript` to 4.5.2. Upgrade oclif dependencies. ([#781](https://github.com/expo/eas-cli/pull/781) by [@dsokal](https://github.com/dsokal))
- Make missing `git` command error message more descriptive. ([#784](https://github.com/expo/eas-cli/pull/784) by [@dsokal](https://github.com/dsokal))
## [0.37.0](https://github.com/expo/eas-cli/releases/tag/v0.37.0) - 2021-11-18
### 🛠 Breaking changes
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- Auto create channel on publish. ([#766](https://github.com/expo/eas-cli/pull/766) by [@jkhales](https://github.com/jkhales))
- Interactively configure Git `user.name` and `user.email`. ([#772](https://github.com/expo/eas-cli/pull/772) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Validate release channel in eas.json. ([#764](https://github.com/expo/eas-cli/pull/764) by [@dsokal](https://github.com/dsokal))
- Make the missing profile error message more descriptive. ([#761](https://github.com/expo/eas-cli/pull/761) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Better error message when missing eas.json. ([#770](https://github.com/expo/eas-cli/pull/770) by [@quinlanj](https://github.com/quinlanj))
## [0.36.1](https://github.com/expo/eas-cli/releases/tag/v0.36.1) - 2021-11-15
### 🐛 Bug fixes
- Fix `dateformat` import issue when building with `--local` flag. ([a520d](https://github.com/expo/eas-cli/commit/a520d1750736d61d6489da6487879f35ed3deb23) by [@dsokal](https://github.com/dsokal))
## [0.36.0](https://github.com/expo/eas-cli/releases/tag/v0.36.0) - 2021-11-15
### 🛠 Breaking changes
- Do not require manual `eas-cli-local-build-plugin` installation. An existing global installation (either with `yarn global add` or `npm install -g`) will not be used anymore. ([#753](https://github.com/expo/eas-cli/pull/753) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Use Keystore Service when `keytool` is not installed. ([#754](https://github.com/expo/eas-cli/pull/754) by [@dsokal](https://github.com/dsokal))
- Add applicationId/bundleIdentifier fields to the submit profile. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
@@ -41,7 +107,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.36.0"
"version": "0.38.2"
}
+6 -4
View File
@@ -1,6 +1,8 @@
{
"private": true,
"workspaces": ["packages/*"],
"workspaces": [
"packages/*"
],
"scripts": {
"build": "lerna run build",
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
@@ -8,7 +10,7 @@
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint . --ext .ts --ext .js",
"lint": "eslint .",
"release": "./scripts/bin/run update-local-plugin && lerna version",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
@@ -24,9 +26,9 @@
"jest": "27.1.0",
"jest-watch-typeahead": "0.6.4",
"lerna": "4.0.0",
"prettier": "2.3.2",
"prettier": "2.4.1",
"ts-jest": "27.0.5",
"typescript": "4.4.2"
"typescript": "4.5.2"
},
"volta": {
"node": "16.10.0",
+13 -64
View File
@@ -73,8 +73,6 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/login.js)_
## `eas account:logout`
log out
@@ -87,8 +85,6 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/logout.js)_
## `eas account:view`
show the username you are logged in as
@@ -101,8 +97,6 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/account/view.js)_
## `eas analytics [STATUS]`
view or change analytics settings
@@ -112,8 +106,6 @@ USAGE
$ eas analytics [STATUS]
```
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/analytics.js)_
## `eas build`
start a build
@@ -146,8 +138,6 @@ OPTIONS
--[no-]wait Wait for build(s) to complete
```
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/index.js)_
## `eas build:cancel [BUILD_ID]`
Cancel a build.
@@ -157,8 +147,6 @@ USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/cancel.js)_
## `eas build:configure`
Configure the project to support EAS Build.
@@ -171,8 +159,6 @@ OPTIONS
-p, --platform=(android|ios|all) Platform to configure
```
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/configure.js)_
## `eas build:list`
list all builds for your project
@@ -204,8 +190,6 @@ OPTIONS
--status=(new|in-queue|in-progress|errored|finished|canceled)
```
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/list.js)_
## `eas build:view [BUILD_ID]`
view a build for your project
@@ -218,8 +202,6 @@ OPTIONS
--json Enable JSON output, non-JSON messages will be printed to stderr
```
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/build/view.js)_
## `eas config`
show the eas.json config
@@ -233,8 +215,6 @@ OPTIONS
--profile=profile
```
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/config.js)_
## `eas credentials`
manage your credentials
@@ -244,8 +224,6 @@ USAGE
$ eas credentials
```
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/credentials.js)_
## `eas device:create`
register new Apple Devices to use for internal distribution
@@ -255,8 +233,6 @@ USAGE
$ eas device:create
```
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/create.js)_
## `eas device:list`
list all registered devices for your account
@@ -269,8 +245,6 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/list.js)_
## `eas device:view [UDID]`
view a device for your project
@@ -280,8 +254,6 @@ USAGE
$ eas device:view [UDID]
```
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/device/view.js)_
## `eas diagnostics`
log environment info to the console
@@ -291,8 +263,6 @@ USAGE
$ eas diagnostics
```
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/diagnostics.js)_
## `eas help [COMMAND]`
display help for eas
@@ -308,7 +278,7 @@ OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.5/src/commands/help.ts)_
## `eas project:info`
@@ -319,8 +289,6 @@ USAGE
$ eas project:info
```
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/project/info.js)_
## `eas project:init`
create or link an EAS project
@@ -333,8 +301,6 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/project/init.js)_
## `eas secret:create`
Create an environment secret on the current project or owner account.
@@ -350,8 +316,6 @@ OPTIONS
--value=value Value of the secret
```
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/create.js)_
## `eas secret:delete`
Delete an environment secret by ID.
@@ -367,8 +331,6 @@ DESCRIPTION
Unsure where to find the secret's ID? Run eas secret:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/delete.js)_
## `eas secret:list`
Lists environment secrets available for your current app
@@ -378,8 +340,6 @@ USAGE
$ eas secret:list
```
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/secret/list.js)_
## `eas submit`
submit build archive to app store
@@ -405,14 +365,13 @@ OPTIONS
--[no-]wait Wait for submission to complete
DESCRIPTION
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
See how to configure submits with eas.json:
https://docs.expo.dev/submit/eas-json/
ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/submit.js)_
## `eas webhook:create`
Create a webhook on the current project.
@@ -422,16 +381,14 @@ USAGE
$ eas webhook:create
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=url Webhook URL
--url=url Webhook URL
```
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/create.js)_
## `eas webhook:delete [ID]`
Delete a webhook on the current project.
@@ -444,8 +401,6 @@ ARGUMENTS
ID ID of the webhook to delete
```
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/delete.js)_
## `eas webhook:list`
List webhooks on the current project.
@@ -455,11 +410,9 @@ USAGE
$ eas webhook:list
OPTIONS
--event=(BUILD) Event type that triggers the webhook
--event=(BUILD|SUBMIT) Event type that triggers the webhook
```
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/list.js)_
## `eas webhook:update`
Create a webhook on the current project.
@@ -469,17 +422,15 @@ USAGE
$ eas webhook:update
OPTIONS
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
--id=id (required) Webhook ID
--event=(BUILD|SUBMIT) Event type that triggers the webhook
--id=id (required) Webhook ID
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
header.
--secret=secret Secret used to create a hash signature of the request payload, provided in the
'Expo-Signature' header.
--url=url Webhook URL
--url=url Webhook URL
```
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/update.js)_
## `eas webhook:view ID`
View a webhook on the current project.
@@ -491,6 +442,4 @@ USAGE
ARGUMENTS
ID ID of the webhook to view
```
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.36.0/packages/eas-cli/src/commands/webhook/view.js)_
<!-- commandsstop -->
+6
View File
@@ -10674,6 +10674,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SUBMIT",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
+12 -13
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.36.0",
"version": "0.38.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -9,24 +9,23 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/config": "6.0.9",
"@expo/config-plugins": "4.0.9",
"@expo/eas-build-job": "0.2.57",
"@expo/eas-json": "^0.36.0",
"@expo/json-file": "8.2.33",
"@expo/pkcs12": "0.0.4",
"@expo/plist": "0.0.14",
"@expo/eas-json": "^0.38.2",
"@expo/json-file": "8.2.34",
"@expo/pkcs12": "0.0.6",
"@expo/plist": "0.0.16",
"@expo/plugin-warn-if-update-available": "1.7.0",
"@expo/prebuild-config": "3.0.3",
"@expo/prebuild-config": "3.0.9",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.0",
"@expo/sdk-runtime-versions": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.5.0",
"@expo/timeago.js": "1.0.0",
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.0",
"@oclif/command": "1.8.3",
"@oclif/config": "1.17.1",
"@oclif/errors": "1.3.5",
"@oclif/plugin-help": "3.2.3",
"@oclif/plugin-help": "3.2.5",
"@urql/core": "2.3.1",
"@urql/exchange-retry": "0.3.0",
"chalk": "4.1.2",
+17 -13
View File
@@ -11,7 +11,7 @@ import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import {
@@ -20,7 +20,7 @@ import {
JobData,
prepareBuildRequestForPlatformAsync,
} from '../build';
import { BuildContext } from '../context';
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
@@ -28,9 +28,9 @@ import { validateAndSyncProjectConfigurationAsync } from './configure';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
): Promise<AndroidBuildContext> {
const { buildProfile } = ctx;
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
@@ -58,21 +58,26 @@ This means that it will most likely produce an AAB and you will not be able to i
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
}
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
return { applicationId, gradleContext };
}
export async function prepareAndroidBuildAsync(
ctx: BuildContext<Platform.ANDROID>
): Promise<BuildRequestSender> {
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
return await ensureAndroidCredentialsAsync(ctx);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildProfile: ctx.buildProfile,
});
},
getMetadataContext: () => ({
gradleContext,
}),
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
@@ -100,8 +105,7 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
}
async function ensureAndroidCredentialsAsync(
ctx: BuildContext<Platform.ANDROID>,
gradleContext?: GradleBuildContext
ctx: BuildContext<Platform.ANDROID>
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
if (!shouldProvideCredentials(ctx)) {
return;
@@ -109,7 +113,7 @@ async function ensureAndroidCredentialsAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
gradleContext
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: {
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import Log from '../../log';
import {
@@ -41,7 +41,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: AndroidBuildProfile;
buildProfile: BuildProfile<Platform.ANDROID>;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
@@ -1,5 +1,5 @@
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
+2 -4
View File
@@ -18,7 +18,7 @@ import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { runLocalBuildAsync } from './local';
import { MetadataContext, collectMetadataAsync } from './metadata';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -39,7 +39,6 @@ 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>;
}
@@ -88,8 +87,7 @@ export async function prepareBuildRequestForPlatformAsync<
bucketKey: await uploadProjectAsync(ctx),
} as const);
const metadataContext = builder.getMetadataContext();
const metadata = await collectMetadataAsync(ctx, metadataContext);
const metadata = await collectMetadataAsync(ctx);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
credentials: credentialsResult?.credentials,
+1 -2
View File
@@ -58,7 +58,6 @@ export async function configureAsync(options: {
platform: RequestedPlatform;
projectDir: string;
}): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await maybeBailOnRepoStatusAsync();
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
@@ -144,7 +143,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
if (await fs.pathExists(easJsonPath)) {
const reader = new EasJsonReader(ctx.projectDir);
await reader.readAndValidateAsync();
await reader.readAsync();
Log.withTick('Validated eas.json');
return;
+22 -118
View File
@@ -1,20 +1,15 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { BuildProfile } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types';
import { RequestedPlatform } from '../platform';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
export interface ConfigureContext {
user: Actor;
@@ -27,6 +22,21 @@ export interface ConfigureContext {
hasIosNativeProject: boolean;
}
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
export interface AndroidBuildContext {
applicationId: string;
gradleContext?: GradleBuildContext;
}
export interface IosBuildContext {
bundleIdentifier: string;
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
applicationTarget: Target;
targets: Target[];
xcodeBuildContext: XcodeBuildContext;
}
export interface BuildContext<T extends Platform> {
accountName: string;
buildProfile: BuildProfile<T>;
@@ -44,112 +54,6 @@ export interface BuildContext<T extends Platform> {
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
}
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const credentialsCtx = new CredentialsContext({
exp,
nonInteractive,
projectDir,
user,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
return {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
}
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: AndroidBuildProfile | IosBuildProfile;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} else {
includesDevClient = false;
}
if (version) {
return { dev_client: includesDevClient, dev_client_version: version };
} else {
return { dev_client: includesDevClient };
}
}
function tryGetDevClientVersion(projectDir: string): string | null {
try {
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
return pkg.version?.toString() ?? null;
} catch {
return null;
}
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
}
+138
View File
@@ -0,0 +1,138 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { TrackingContext } from '../analytics/common';
import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { findAccountByName } from '../user/Account';
import { ensureLoggedInAsync } from '../user/actions';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
buildProfile,
clearCache = false,
local,
nonInteractive = false,
platform,
projectDir,
skipProjectConfiguration = false,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
clearCache: boolean;
local: boolean;
nonInteractive: boolean;
platform: T;
projectDir: string;
skipProjectConfiguration: boolean;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const credentialsCtx = new CredentialsContext({
exp,
nonInteractive,
projectDir,
user,
});
const devClientProperties = getDevClientEventProperties({
platform,
projectDir,
buildProfile,
});
const trackingCtx = {
tracking_id: uuidv4(),
platform,
...(accountId && { account_id: accountId }),
account_name: accountName,
project_id: projectId,
project_type: workflow,
...devClientProperties,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const commonContext: CommonContext<T> = {
accountName,
buildProfile,
buildProfileName,
clearCache,
credentialsCtx,
exp,
local,
nonInteractive,
platform,
projectDir,
projectId,
projectName,
skipProjectConfiguration,
trackingCtx,
user,
workflow,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
return {
...common,
android: await createAndroidContextAsync(common),
} as BuildContext<T>;
} else {
const common = commonContext as CommonContext<Platform.IOS>;
return {
...common,
ios: await createIosContextAsync(common),
} as BuildContext<T>;
}
}
function getDevClientEventProperties({
platform,
projectDir,
buildProfile,
}: {
platform: Platform;
projectDir: string;
buildProfile: BuildProfile;
}): Partial<TrackingContext> {
let includesDevClient;
const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
} else if (buildProfile.developmentClient) {
includesDevClient = true;
} else {
includesDevClient = false;
}
if (version) {
return { dev_client: includesDevClient, dev_client_version: version };
} else {
return { dev_client: includesDevClient };
}
}
function tryGetDevClientVersion(projectDir: string): string | null {
try {
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
return pkg.version?.toString() ?? null;
} catch {
return null;
}
}
@@ -4,9 +4,9 @@ import { IOSConfig } from '@expo/config-plugins';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
@@ -59,7 +59,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
}
async function writeExpoPlistAsync(
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import { readPlistAsync } from '../../../utils/plist';
import {
BumpStrategy,
bumpVersionAsync,
+31 -31
View File
@@ -8,24 +8,17 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext } from '../context';
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import { IosMetadataContext } from '../metadata';
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
import { validateAndSyncProjectConfigurationAsync } from './configure';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
interface BuildConfiguration {
targetName?: string;
buildConfiguration?: string;
buildSettings: XCBuildConfiguration['buildSettings'];
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
@@ -50,29 +43,42 @@ export async function prepareIosBuildAsync(
},
xcodeBuildContext
);
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
return {
bundleIdentifier: applicationTarget.bundleIdentifier,
applicationTarget,
applicationTargetBuildSettings,
targets,
xcodeBuildContext,
};
}
export async function prepareIosBuildAsync(
ctx: BuildContext<Platform.IOS>
): Promise<BuildRequestSender> {
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return ensureIosCredentialsAsync(ctx, targets);
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
ensureProjectConfiguredAsync: async () => {
await validateAndSyncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile,
buildSettings: buildConfiguration.buildSettings,
buildProfile: ctx.buildProfile,
buildSettings: ctx.ios.applicationTargetBuildSettings,
});
},
getMetadataContext: (): IosMetadataContext => {
return buildConfiguration;
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Job> => {
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
return await prepareJobAsync(ctx, {
...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
});
},
sendBuildRequestAsync: async (
appId: string,
@@ -90,23 +96,17 @@ export async function prepareIosBuildAsync(
});
}
function resolveBuildConfiguration(
ctx: BuildContext<Platform.IOS>,
targets: Target[]
): BuildConfiguration {
function resolveBuildSettings(
ctx: CommonContext<Platform.IOS>,
applicationTarget: Target
): XCBuildConfiguration['buildSettings'] {
if (ctx.workflow === Workflow.MANAGED) {
return { buildSettings: {} };
return {};
}
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,
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
};
return xcBuildConfiguration?.buildSettings ?? {};
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
@@ -36,7 +36,7 @@ export async function validateAndSyncProjectConfigurationAsync({
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: IosBuildProfile;
buildProfile: BuildProfile<Platform.IOS>;
buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
+13 -4
View File
@@ -8,9 +8,9 @@ import type { XCBuildConfiguration } from 'xcode';
import Log from '../../log';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync } from '../utils/version';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
APP_VERSION,
@@ -128,7 +128,11 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
};
} catch {
} catch (err: any) {
Log.warn('Failed to read app versions.');
Log.debug(err);
Log.warn(err.message);
Log.warn('Proceeding anyway...');
return {};
}
}
@@ -174,7 +178,7 @@ async function readInfoPlistAsync(
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
}
async function writeInfoPlistAsync({
@@ -197,7 +201,12 @@ function ensureStaticConfigExists(projectDir: string): void {
}
}
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
export function evaluateTemplateString(
s: string,
buildSettings: XCBuildConfiguration['buildSettings']
): string {
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
const vars = { ...buildSettings };
return s.replace(/\$\((\w+)\)/g, (match, key) => {
if (vars.hasOwnProperty(key)) {
const value = String(vars[key]);
+16 -4
View File
@@ -1,17 +1,18 @@
import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import semver from 'semver';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.47';
const PLUGIN_PACKAGE_VERSION = '0.0.50';
export async function runLocalBuildAsync(job: Job): Promise<void> {
const { command, args } = getCommandAndArgs(job);
const { command, args } = await getCommandAndArgsAsync(job);
await spawnAsync(command, args, {
stdio: 'inherit',
});
}
function getCommandAndArgs(job: Job): { command: string; args: string[] } {
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
@@ -19,9 +20,20 @@ function getCommandAndArgs(job: Job): { command: string; args: string[] } {
args: [jobBase64],
};
} else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
// prompting the user to install a package that is used for the first time
args.unshift('-y');
}
return {
command: 'npx',
args: ['-y', `${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64],
args,
};
}
}
async function isAtLeastNpm7Async(): Promise<boolean> {
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
return semver.gte(version, '7.0.0');
}
+22 -35
View File
@@ -1,10 +1,8 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { getApplicationIdAsync } from '../project/android/applicationId';
import { GradleBuildContext } from '../project/android/gradle';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import Log from '../log';
import { getUsername } from '../project/projectUtils';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
@@ -22,22 +20,20 @@ import {
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { isExpoUpdatesInstalled } from './utils/updates';
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
? AndroidMetadataContext
: IosMetadataContext;
export interface AndroidMetadataContext {
gradleContext?: GradleBuildContext;
}
export interface IosMetadataContext {
buildSettings: XCBuildConfiguration['buildSettings'];
targetName?: string;
buildConfiguration?: string;
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
function getRuntimeVersionNullable(
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
): string | null {
try {
return Updates.getRuntimeVersion(config, platform);
} catch (e) {
Log.debug(e);
return null;
}
}
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
ctx: BuildContext<T>
): Promise<Metadata> {
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
@@ -46,16 +42,16 @@ export async function collectMetadataAsync<T extends Platform>(
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx, platformContext)),
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: ctx.exp.runtimeVersion,
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
@@ -70,15 +66,14 @@ export async function collectMetadataAsync<T extends Platform>(
}
async function maybeResolveVersionsAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
ctx: BuildContext<T>
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
const iosContext = ctx as BuildContext<Platform.IOS>;
return await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext?.buildSettings ?? {}
iosContext.ios.applicationTargetBuildSettings
);
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
@@ -88,19 +83,11 @@ async function maybeResolveVersionsAsync<T extends Platform>(
}
}
async function resolveAppIdentifierAsync<T extends Platform>(
ctx: BuildContext<T>,
platformContext: MetadataContext<T>
): Promise<string> {
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
if (ctx.platform === Platform.IOS) {
const iosContext = platformContext as IosMetadataContext;
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
targetName: iosContext.targetName,
buildConfiguration: iosContext.buildConfiguration,
});
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
} else {
const androidContext = platformContext as AndroidMetadataContext;
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
}
}
@@ -1,5 +1,4 @@
import { Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { error } from '@oclif/errors';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
@@ -21,7 +20,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
}: {
projectDir: string;
nonInteractive?: boolean;
buildProfiles?: ProfileData<BuildProfile>[];
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
return;
@@ -136,6 +136,7 @@ export async function reviewAndCommitChangesAsync(
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
});
Log.withTick('Committed changes.');
return;
+3 -3
View File
@@ -4,9 +4,9 @@ import path from 'path';
import Log, { learnMore } from '../log';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import { CommonContext } from './context';
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
@@ -16,7 +16,7 @@ export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
}
export async function checkGoogleServicesFileAsync<T extends Platform>(
ctx: BuildContext<T>
ctx: CommonContext<T>
): Promise<void> {
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
return;
@@ -8,7 +8,6 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
@@ -27,8 +26,6 @@ export default abstract class EasCommand extends Command {
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
EasJsonReader.setLog(Log);
await initAnalyticsAsync();
await this.applyCliConfigAsync();
@@ -35,10 +35,13 @@ import { formatUpdate } from '../../update/utils';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { getVcsClient } from '../../vcs';
import { createUpdateChannelOnAppAsync } from '../channel/create';
import { createUpdateBranchOnAppAsync } from './create';
import { listBranchesAsync } from './list';
import { viewUpdateBranchAsync } from './view';
const EAS_UPDATE_URL = 'https://u.expo.dev';
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type PlatformFlag = PublishPlatform | 'all';
@@ -70,6 +73,34 @@ async function getUpdateGroupAsync({
return updatesByGroup;
}
async function ensureChannelExistsAsync({
appId,
branchId,
channelName,
}: {
appId: string;
branchId: string;
channelName: string;
}): Promise<void> {
try {
await createUpdateChannelOnAppAsync({
appId,
channelName,
branchId,
});
Log.withTick(
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
);
} catch (e: any) {
const isIgnorableError =
e.graphQLErrors?.length === 1 &&
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
if (!isIgnorableError) {
throw e;
}
}
}
async function ensureBranchExistsAsync({
appId,
name: branchName,
@@ -90,10 +121,13 @@ async function ensureBranchExistsAsync({
const updateBranch = app?.byId.updateBranchByName;
if (updateBranch) {
const { id, updates } = updateBranch;
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
return { id, updates };
}
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
return { id: newUpdateBranch.id, updates: [] };
}
@@ -172,8 +206,8 @@ export default class BranchPublish extends EasCommand {
});
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp);
await checkEASUpdateURLIsSetAsync(exp);
if (!branchName && autoFlag) {
branchName =
@@ -441,3 +475,19 @@ function formatUpdateTitle(
'mmm dd HH:MM'
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
}
async function getEASUpdateURLAsync(exp: ExpoConfig): Promise<string> {
const projectId = await getProjectIdAsync(exp);
return new URL(projectId, EAS_UPDATE_URL).href;
}
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
const configuredURL = exp.updates?.url;
const expectedURL = await getEASUpdateURLAsync(exp);
if (configuredURL !== expectedURL) {
throw new Error(
`The update URL is incorrectly configured for EAS Update. Please set updates.url to ${expectedURL} in your app.json.`
);
}
}
@@ -7,6 +7,7 @@ import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static description = 'Configure the project to support EAS Build.';
@@ -21,11 +22,14 @@ export default class BuildConfigure extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = this.parse(BuildConfigure);
Log.log(
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
);
Log.newLine();
await getVcsClient().ensureRepoExistsAsync();
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
+37 -36
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { BuildProfile, EasJsonReader, SubmitProfile } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
@@ -10,13 +10,13 @@ import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from '../../build/android/build';
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
import { ensureProjectConfiguredAsync } from '../../build/configure';
import { BuildContext, createBuildContextAsync } from '../../build/context';
import { BuildContext } from '../../build/context';
import { createBuildContextAsync } from '../../build/createContext';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
AppPlatform,
BuildFragment,
@@ -32,10 +32,6 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../../platform';
import {
EAS_UNAVAILABLE_MESSAGE,
isEasEnabledForProjectAsync,
} from '../../project/isEasEnabledForProject';
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
import { findProjectRootAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
@@ -68,7 +64,7 @@ interface RawBuildFlags {
interface BuildFlags {
requestedPlatform: RequestedPlatform;
skipProjectConfiguration: boolean;
profile: string | null;
profile?: string;
nonInteractive: boolean;
local: boolean;
wait: boolean;
@@ -152,13 +148,11 @@ export default class Build extends EasCommand {
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
const platforms = toPlatforms(requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const buildProfiles = await getProfilesAsync({
type: 'build',
projectDir,
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readBuildProfileAsync(platform, profileName);
},
profileName: flags.profile ?? undefined,
});
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
@@ -169,7 +163,7 @@ export default class Build extends EasCommand {
const startedBuilds: {
build: BuildFragment;
buildProfile: ProfileData<BuildProfile>;
buildProfile: ProfileData<'build'>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
@@ -196,16 +190,26 @@ export default class Build extends EasCommand {
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
projectDir,
platforms,
profileName: flags.submitProfile,
type: 'submit',
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
submitProfiles.find(
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await this.prepareAndStartSubmissionAsync({
build: startedBuild.build,
credentialsCtx: nullthrows(
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
),
flags,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
});
submissions.push(submission);
}
@@ -268,7 +272,7 @@ export default class Build extends EasCommand {
return {
requestedPlatform,
skipProjectConfiguration: flags['skip-project-configuration'],
profile: profile ?? null,
profile,
nonInteractive,
local: flags['local'],
wait: flags['wait'],
@@ -288,7 +292,7 @@ export default class Build extends EasCommand {
projectDir: string;
flags: BuildFlags;
moreBuilds: boolean;
buildProfile: ProfileData<BuildProfile>;
buildProfile: ProfileData<'build'>;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
@@ -316,10 +320,6 @@ export default class Build extends EasCommand {
this.metroConfigValidated = true;
}
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
}
const build = await this.startBuildAsync(buildCtx);
return {
build,
@@ -339,31 +339,32 @@ export default class Build extends EasCommand {
private async prepareAndStartSubmissionAsync({
build,
credentialsCtx,
flags,
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
nonInteractive,
}: {
build: BuildFragment;
credentialsCtx: CredentialsContext;
flags: BuildFlags;
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: ProfileData<BuildProfile>;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
nonInteractive: boolean;
}): Promise<SubmissionFragment> {
const easJsonReader = new EasJsonReader(projectDir);
const platform = toPlatform(build.platform);
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
const submissionCtx = await createSubmissionContextAsync({
platform,
projectDir,
projectId: build.project.id,
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive: flags.nonInteractive,
env: buildProfile.profile.env,
credentialsCtx,
nonInteractive,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
});
if (moreBuilds) {
@@ -397,7 +398,7 @@ export async function handleDeprecatedEasJsonAsync(
return;
}
const easJsonReader = new EasJsonReader(projectDir);
const rawEasJson = await easJsonReader.readRawAsync();
const rawEasJson = await easJsonReader.readAsync();
if (rawEasJson?.cli) {
return;
}
@@ -20,7 +20,7 @@ import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
export async function createUpdateChannelOnAppAsync({
appId,
channelName,
branchId,
+1 -1
View File
@@ -55,7 +55,7 @@ export default class Config extends EasCommand {
},
]));
const profile = await reader.readBuildProfileAsync(platform, profileName);
const profile = await reader.getBuildProfileAsync(platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.log(getProjectConfigDescription(projectDir));
@@ -9,10 +9,6 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -57,12 +53,6 @@ export default class EnvironmentSecretCreate extends EasCommand {
const { slug } = exp;
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (!scope) {
const validationMessage = 'Secret scope may not be empty.';
@@ -10,10 +10,6 @@ import {
EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -37,12 +33,6 @@ Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
let {
flags: { id },
} = this.parse(EnvironmentSecretDelete);
@@ -6,10 +6,6 @@ import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
} from '../../project/isEasEnabledForProject';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -25,12 +21,6 @@ export default class EnvironmentSecretList extends EasCommand {
const projectId = await getProjectIdAsync(exp);
const projectAccountName = await getProjectAccountNameAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
if (!projectDir) {
throw new Error("Please run this command inside your project's directory");
}
+2 -12
View File
@@ -1,5 +1,4 @@
import { getConfig } from '@expo/config';
import { EasJsonReader } from '@expo/eas-json';
import { flags } from '@oclif/command';
import { error } from '@oclif/errors';
import chalk from 'chalk';
@@ -15,7 +14,6 @@ import {
selectRequestedPlatformAsync,
toPlatforms,
} from '../platform';
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
@@ -96,20 +94,12 @@ See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
if (!(await isEasEnabledForProjectAsync(projectId))) {
warnEasUnavailable();
process.exitCode = 1;
return;
}
const platforms = toPlatforms(flags.requestedPlatform);
const easJsonReader = new EasJsonReader(projectDir);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
projectDir,
platforms,
profileName: flags.profile,
async readProfileAsync(platform, profileName) {
return await easJsonReader.readSubmitProfileAsync(platform, profileName);
},
});
const submissions: SubmissionFragment[] = [];
@@ -14,8 +14,7 @@ export default class WebhookCreate extends EasCommand {
static flags = {
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
options: [WebhookType.Build, WebhookType.Submit],
}),
url: flags.string({
description: 'Webhook URL',
@@ -20,7 +20,7 @@ export default class WebhookList extends EasCommand {
static flags = {
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
options: [WebhookType.Build, WebhookType.Submit],
}),
};
@@ -18,8 +18,7 @@ export default class WebhookUpdate extends EasCommand {
}),
event: flags.enum({
description: 'Event type that triggers the webhook',
options: [WebhookType.Build],
default: WebhookType.Build,
options: [WebhookType.Build, WebhookType.Submit],
}),
url: flags.string({
description: 'Webhook URL',
@@ -150,6 +150,30 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
expect(results.disabled).toStrictEqual([]);
});
// It's unclear what is happening in this issue, so for now we can provide a more
// helpful error message until we can get a reproducible example.
// https://github.com/expo/eas-cli/issues/740
it('throw useful error message on unexpected error', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => []),
updateBundleIdCapabilityAsync: jest.fn(() => {
throw new Error(`bundle 'XXX333' cannot be deleted. Delete all the Apps`);
}),
attributes: {
identifier: 'dev.expo.app',
},
id: 'XXX333',
} as any;
await expect(
syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
})
).rejects.toThrowError(
`https://developer.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
);
});
it('cannot skip complex duplicates', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
@@ -11,6 +11,7 @@ import {
} from '@expo/apple-utils';
import { JSONObject, JSONValue } from '@expo/json-file';
import getenv from 'getenv';
import { inspect } from 'util';
import Log from '../../../log';
@@ -100,7 +101,20 @@ export async function syncCapabilitiesForEntitlementsAsync(
);
if (modifiedRequest.length) {
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
Log.debug(`Patch Request:`, inspect(modifiedRequest, { depth: null, colors: true }));
try {
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
} catch (error: any) {
if (error.message.match(/bundle '[\w\d]+' cannot be deleted. Delete all the Apps/)) {
Log.error(
'Failed to patch capabilities:',
inspect(modifiedRequest, { depth: null, colors: true })
);
throw new Error(
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
);
}
}
}
return { enabled: enabledCapabilityNames, disabled: disabledCapabilityNames };
@@ -1,9 +1,9 @@
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { JSONObject } from '@expo/json-file';
import plist from '@expo/plist';
import { getPrebuildConfig } from '@expo/prebuild-config';
import fs from 'fs-extra';
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
import { readPlistAsync } from '../../../utils/plist';
export async function getManagedEntitlementsJsonAsync(
projectDir: string,
@@ -15,7 +15,7 @@ export async function getManagedEntitlementsJsonAsync(
...process.env,
...env,
};
const { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
const { exp } = await getPrebuildConfigAsync(projectDir, { platforms: ['ios'] });
const expWithMods = await compileModsAsync(exp, {
projectRoot: projectDir,
@@ -43,12 +43,11 @@ export async function resolveEntitlementsJsonAsync(
}
async function getEntitlementsJsonAsync(projectDir: string): Promise<JSONObject | null> {
try {
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
if (entitlementsPath) {
const entitlementsContents = await fs.readFile(entitlementsPath, 'utf8');
return plist.parse(entitlementsContents);
}
} catch {}
return null;
const entitlementsPath = IOSConfig.Paths.getEntitlementsPath(projectDir);
if (entitlementsPath) {
const plist = await readPlistAsync(entitlementsPath);
return plist ? (plist as unknown as JSONObject) : null;
} else {
return null;
}
}
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidBuildProfile } from '@expo/eas-json';
import { BuildProfile } from '@expo/eas-json';
import assert from 'assert';
import Log, { learnMore } from '../../log';
@@ -142,7 +142,7 @@ export class ManageAndroid {
private async createProjectContextAsync(ctx: CredentialsContext): Promise<{
gradleContext?: GradleBuildContext;
buildProfile?: AndroidBuildProfile;
buildProfile?: BuildProfile<Platform.ANDROID>;
}> {
if (!ctx.hasProjectContext) {
return {};
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { IosBuildProfile } from '@expo/eas-json';
import { BuildProfile } from '@expo/eas-json';
import assert from 'assert';
import nullthrows from 'nullthrows';
@@ -155,7 +155,11 @@ export class ManageIos {
private async createProjectContextAsync(
ctx: CredentialsContext,
account: Account
): Promise<{ app: App | null; targets: Target[] | null; buildProfile: IosBuildProfile | null }> {
): Promise<{
app: App | null;
targets: Target[] | null;
buildProfile: BuildProfile<Platform.IOS> | null;
}> {
if (!ctx.hasProjectContext) {
return {
app: null,
@@ -219,7 +223,7 @@ export class ManageIos {
ctx: CredentialsContext,
app: App,
targets: Target[],
buildProfile: IosBuildProfile,
buildProfile: BuildProfile<Platform.IOS>,
action: IosActionType
): Promise<void> {
if (action === IosActionType.SetUpBuildCredentials) {
@@ -14,7 +14,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
async runAsync(ctx: CredentialsContext): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(this.platform, profileName);
const easConfig = await this.easJsonReader.getBuildProfileAsync<T>(this.platform, profileName);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
@@ -1,11 +1,12 @@
import { IosBuildProfile } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { IosDistributionType as IosDistributionTypeGraphql } from '../../graphql/generated';
import { promptAsync } from '../../prompts';
import { CredentialsContext } from '../context';
export class SelectIosDistributionTypeGraphqlFromBuildProfile {
constructor(private buildProfile: IosBuildProfile) {}
constructor(private buildProfile: BuildProfile<Platform.IOS>) {}
async runAsync(ctx: CredentialsContext): Promise<IosDistributionTypeGraphql> {
const { distribution, simulator } = this.buildProfile;
@@ -1,4 +1,5 @@
import { IosBuildProfile } from '@expo/eas-json';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { IosDistributionType as IosDistributionTypeGraphql } from '../../../graphql/generated';
import { createCtxMock } from '../../__tests__/fixtures-context';
@@ -8,7 +9,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('errors with a simulator distribution', async () => {
const buildProfile = {
simulator: true,
} as IosBuildProfile;
} as BuildProfile<Platform.IOS>;
const ctx = createCtxMock({
nonInteractive: false,
@@ -22,7 +23,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
it('returns APP_STORE type with a store distribution', async () => {
const buildProfile = {
distribution: 'store',
} as IosBuildProfile;
} as BuildProfile<Platform.IOS>;
const ctx = createCtxMock({
nonInteractive: false,
@@ -37,7 +38,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const buildProfile = {
distribution: 'internal',
enterpriseProvisioning: 'universal',
} as IosBuildProfile;
} as BuildProfile<Platform.IOS>;
const ctx = createCtxMock({
nonInteractive: false,
@@ -52,7 +53,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
const buildProfile = {
distribution: 'internal',
enterpriseProvisioning: 'adhoc',
} as IosBuildProfile;
} as BuildProfile<Platform.IOS>;
const ctx = createCtxMock({
nonInteractive: false,
+2 -1
View File
@@ -1411,7 +1411,8 @@ export type WebhookFilter = {
};
export enum WebhookType {
Build = 'BUILD'
Build = 'BUILD',
Submit = 'SUBMIT'
}
export type Webhook = {
@@ -54,7 +54,7 @@ describe(getApplicationIdAsync, () => {
'/app'
);
await expect(getApplicationIdAsync('/app', {} as any, undefined)).rejects.toThrowError(
/Could not read application id/
/Failed to find 'build.gradle' /
);
});
@@ -69,7 +69,7 @@ describe(getApplicationIdAsync, () => {
await expect(
getApplicationIdAsync('/app', {} as any, { moduleName: 'app' })
).rejects.toThrowError(/Could not read application id/);
).rejects.toThrowError(/Could not read applicationId/);
});
});
@@ -112,7 +112,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
);
await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any)
).rejects.toThrowError(/we can't update this file programatically/);
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('prompts for the Android package if using app.json', async () => {
vol.fromJSON(
@@ -15,7 +15,7 @@ import { resolveWorkflowAsync } from '../workflow';
import { GradleBuildContext } from './gradle';
import * as gradleUtils from './gradleUtils';
const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
projectDir: string,
@@ -33,6 +33,51 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
}
}
export class AmbiguousApplicationIdError extends Error {
constructor(message?: string) {
super(message ?? 'Could not resolve applicationId.');
}
}
export async function getApplicationIdFromBareAsync(
projectDir: string,
gradleContext?: GradleBuildContext
): Promise<string> {
const errorMessage = 'Could not read applicationId from Android project.';
if (gradleContext) {
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
const applicationIdSuffix = gradleUtils.resolveConfigValue(
buildGradle,
'applicationIdSuffix',
gradleContext.flavor
);
if (applicationIdSuffix) {
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
}
const applicationId = gradleUtils.resolveConfigValue(
buildGradle,
'applicationId',
gradleContext.flavor
);
return nullthrows(applicationId, errorMessage);
} else {
// should return value only if productFlavors are not used
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
if (buildGradle.match(/applicationIdSuffix/)) {
throw new Error('"applicationIdSuffix" in app/build.gradle is not supported.');
}
if (buildGradle.match(/productFlavors/)) {
throw new AmbiguousApplicationIdError(
'Failed to autodetect applicationId in multi-flavor project.'
);
}
return nullthrows(matchResult?.[1], errorMessage);
}
}
export async function getApplicationIdAsync(
projectDir: string,
exp: ExpoConfig,
@@ -42,33 +87,7 @@ export async function getApplicationIdAsync(
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const errorMessage = 'Could not read application id from Android project.';
if (gradleContext) {
const buildGradle = await gradleUtils.getAppBuildGradleAsync(projectDir);
const applicationId = gradleUtils.resolveConfigValue(
buildGradle,
'applicationId',
gradleContext.flavor
);
return nullthrows(applicationId, errorMessage);
} else {
// fallback to best effort approach, this logic can be dropped when we start supporting
// modules different than 'app' and 'flavorDimensions'
let buildGradlePath = null;
try {
buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
} catch {}
if (!buildGradlePath || !(await fs.pathExists(buildGradlePath))) {
throw new Error(errorMessage);
}
const buildGradle = await fs.readFile(buildGradlePath, 'utf8');
const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/);
const applicationId = nullthrows(matchResult?.[1], errorMessage);
Log.warn(`Unable to detect applicationId`);
Log.warn(`Falling back to best effort approach, using applicationId ${applicationId}`);
return applicationId;
}
return getApplicationIdFromBareAsync(projectDir, gradleContext);
} else {
const applicationId = AndroidConfig.Package.getPackage(exp);
if (!applicationId || !isApplicationIdValid(applicationId)) {
@@ -91,7 +110,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
throw new Error(
`"android.package" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
`"android.package" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
);
}
@@ -125,7 +144,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
return packageName;
}
function isApplicationIdValid(applicationId: string): boolean {
export function isApplicationIdValid(applicationId: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
}
@@ -27,7 +27,7 @@ export async function resolveGradleBuildContextAsync(
parsedGradleCommand.moduleName !== gradleUtils.DEFAULT_MODULE_NAME
) {
Log.warn(
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior`
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior.`
);
}
return {
@@ -38,7 +38,8 @@ export async function resolveGradleBuildContextAsync(
return { moduleName: gradleUtils.DEFAULT_MODULE_NAME };
}
} catch (err: any) {
Log.warn(`Unable to read project config from app/build.gradle: ${err.message}`);
Log.warn(`Unable to read gradle project config: ${err.message}.`);
Log.warn('Values from app/build.gradle might be resolved incorrectly.');
return undefined;
}
} else {
@@ -12,6 +12,7 @@ interface GradleCommand {
interface Config {
applicationId?: string;
applicationIdSuffix?: string; // unsupported value
versionCode?: string;
versionName?: string;
}
@@ -95,7 +95,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
);
await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any)
).rejects.toThrowError(/we can't update this file programatically/);
).rejects.toThrowError(/we can't update this file programmatically/);
});
it('prompts for the bundle identifier if using app.json', async () => {
vol.fromJSON(
@@ -12,7 +12,7 @@ import { ensureLoggedInAsync } from '../../user/actions';
import { getProjectConfigDescription, getUsername } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow';
const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
projectDir: string,
@@ -28,22 +28,39 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
}
}
export class AmbiguousBundleIdentifierError extends Error {
constructor(message?: string) {
super(message ?? 'Could not resolve bundle identifier.');
}
}
export async function getBundleIdentifierAsync(
projectDir: string,
exp: ExpoConfig,
{ targetName, buildConfiguration }: { targetName?: string; buildConfiguration?: string } = {}
xcodeContext?: { targetName?: string; buildConfiguration?: string }
): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir, {
targetName,
buildConfiguration,
});
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
const isMultiScheme = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir).length > 1;
const isMultiTarget =
IOSConfig.Target.getNativeTargets(xcodeProject).filter(([, target]) =>
IOSConfig.Target.isTargetOfType(target, IOSConfig.Target.TargetType.APPLICATION)
).length > 1;
if (!xcodeContext && isMultiScheme && isMultiTarget) {
throw new AmbiguousBundleIdentifierError(
"Multiple schemes and targets found in Xcode project, bundle identifier couldn't be resolved."
);
}
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir,
xcodeContext ?? {}
);
const buildConfigurationDesc =
targetName && buildConfiguration
? ` (target = ${targetName}, build configuration = ${buildConfiguration})`
xcodeContext?.targetName && xcodeContext?.buildConfiguration
? ` (target = ${xcodeContext.targetName}, build configuration = ${xcodeContext.buildConfiguration})`
: '';
assert(
bundleIdentifier,
@@ -58,13 +75,17 @@ export async function getBundleIdentifierAsync(
} else {
// TODO: the following asserts are only temporary until we support app extensions in managed projects
assert(
!targetName || targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
'targetName cannot be set to an arbitrary value for managed projects'
!xcodeContext?.targetName ||
xcodeContext?.targetName === IOSConfig.XcodeUtils.sanitizedName(exp.name),
'targetName cannot be set to an arbitrary value for managed projects.'
);
assert(!buildConfiguration, 'buildConfiguration cannot be passed for managed projects');
const bundleIdentifer = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (!bundleIdentifer || !isBundleIdentifierValid(bundleIdentifer)) {
if (bundleIdentifer) {
assert(
!xcodeContext?.buildConfiguration,
'buildConfiguration cannot be passed for managed projects.'
);
const bundleIdentifier = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (!bundleIdentifier || !isBundleIdentifierValid(bundleIdentifier)) {
if (bundleIdentifier) {
Log.warn(INVALID_BUNDLE_IDENTIFIER_MESSAGE);
}
throw new Error(
@@ -73,7 +94,7 @@ export async function getBundleIdentifierAsync(
)} and run this command again.`
);
} else {
return bundleIdentifer;
return bundleIdentifier;
}
}
}
@@ -86,7 +107,7 @@ async function configureBundleIdentifierAsync(
// we can't automatically update app.config.js
if (paths.dynamicConfigPath) {
throw new Error(
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programatically. Add the value on your own and run this command again.`
`"ios.bundleIdentifier" is not defined in your app.config.js and we can't update this file programmatically. Add the value on your own and run this command again.`
);
}
@@ -120,7 +141,7 @@ async function configureBundleIdentifierAsync(
return bundleIdentifier;
}
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
export function isBundleIdentifierValid(bundleIdentifier: string): boolean {
return /^[a-zA-Z0-9-.]+$/.test(bundleIdentifier);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { IosBuildProfile } from '@expo/eas-json';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import Log from '../../log';
@@ -20,7 +20,7 @@ export async function resolveXcodeBuildContextAsync(
projectDir,
nonInteractive,
}: { exp: ExpoConfig; projectDir: string; nonInteractive: boolean },
buildProfile: IosBuildProfile
buildProfile: BuildProfile<Platform.IOS>
): Promise<XcodeBuildContext> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
@@ -1,32 +0,0 @@
import chalk from 'chalk';
import { apiClient } from '../api';
import Log from '../log';
/**
* Checks if the project is allowed for using EAS services.
* THIS IS A TEMPORARY STEP NEEDED FOR OPEN PREVIEW
* @returns boolean
*/
export async function isEasEnabledForProjectAsync(projectId: string): Promise<boolean> {
try {
const {
data: { enabled },
} = await apiClient.get(`projects/${projectId}/eas-enabled`).json();
return enabled;
} catch (error: any) {
if (error.response?.statusCode === 404) {
return true;
} else {
throw error;
}
}
}
export const EAS_UNAVAILABLE_MESSAGE = `Your account doesn't have access to Expo Application Services (EAS) features. Enroll in EAS to give it a try: ${chalk.underline(
'https://expo.dev/eas'
)}`;
export function warnEasUnavailable(): void {
Log.warn(EAS_UNAVAILABLE_MESSAGE);
}
@@ -1,37 +0,0 @@
import { promptAsync } from '../../prompts';
export enum AndroidPackageSourceType {
userDefined,
prompt,
}
interface AndroidPackageSourceBase {
sourceType: AndroidPackageSourceType;
}
interface AndroidPackageUserDefinedSource extends AndroidPackageSourceBase {
sourceType: AndroidPackageSourceType.userDefined;
androidPackage: string;
}
interface AndroidPackagePromptSource extends AndroidPackageSourceBase {
sourceType: AndroidPackageSourceType.prompt;
}
export type AndroidPackageSource = AndroidPackageUserDefinedSource | AndroidPackagePromptSource;
export async function getAndroidPackageAsync(source: AndroidPackageSource): Promise<string> {
if (source.sourceType === AndroidPackageSourceType.userDefined) {
return source.androidPackage;
} else if (source.sourceType === AndroidPackageSourceType.prompt) {
const { androidPackage } = await promptAsync({
name: 'androidPackage',
message: 'Android package name:',
type: 'text',
validate: (val: string) => val !== '' || 'Package name cannot be empty!',
});
return androidPackage;
} else {
throw new Error('This should never happen');
}
}
@@ -8,13 +8,14 @@ import {
SubmissionFragment,
} from '../../graphql/generated';
import Log from '../../log';
import { getApplicationIdAsync } from '../../project/android/applicationId';
import * as gradleUtils from '../../project/android/gradleUtils';
import {
AmbiguousApplicationIdError,
getApplicationIdAsync,
} from '../../project/android/applicationId';
import capitalize from '../../utils/expodash/capitalize';
import { ArchiveSource } from '../ArchiveSource';
import { resolveArchiveSource } from '../commons';
import { SubmissionContext } from '../context';
import { AndroidPackageSource, AndroidPackageSourceType } from './AndroidPackageSource';
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
@@ -29,19 +30,12 @@ export default class AndroidSubmitCommand {
}
private async getAndroidSubmissionOptionsAsync(): Promise<AndroidSubmissionOptions> {
const androidPackageSource = await this.resolveAndroidPackageSourceAsync();
const track = this.resolveTrack();
const releaseStatus = this.resolveReleaseStatus();
const archiveSource = this.resolveArchiveSource();
const serviceAccountSource = this.resolveServiceAccountSource();
const serviceAccountSource = await this.resolveServiceAccountSourceAsync();
const errored = [
androidPackageSource,
track,
releaseStatus,
archiveSource,
serviceAccountSource,
].filter(r => !r.ok);
const errored = [track, releaseStatus, archiveSource, serviceAccountSource].filter(r => !r.ok);
if (errored.length > 0) {
const message = errored.map(err => err.reason?.message).join('\n');
Log.error(message);
@@ -50,7 +44,6 @@ export default class AndroidSubmitCommand {
return {
projectId: this.ctx.projectId,
androidPackageSource: androidPackageSource.enforceValue(),
track: track.enforceValue(),
releaseStatus: releaseStatus.enforceValue(),
archiveSource: archiveSource.enforceValue(),
@@ -59,29 +52,19 @@ export default class AndroidSubmitCommand {
};
}
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<string | undefined> {
private async maybeGetAndroidPackageFromCurrentProjectAsync(): Promise<Result<string | null>> {
try {
return await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp, {
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
});
} catch {
return undefined;
}
}
private async resolveAndroidPackageSourceAsync(): Promise<Result<AndroidPackageSource>> {
const androidPackage = await this.maybeGetAndroidPackageFromCurrentProjectAsync();
if (androidPackage) {
return result({
sourceType: AndroidPackageSourceType.userDefined,
androidPackage,
});
} else if (this.ctx.nonInteractive) {
return result(new Error("Couldn't resolve the Android package."));
} else {
return result({
sourceType: AndroidPackageSourceType.prompt,
});
return result(await getApplicationIdAsync(this.ctx.projectDir, this.ctx.exp));
} catch (error: any) {
if (error instanceof AmbiguousApplicationIdError) {
Log.warn(
'"applicationId" is ambiguous, specify it via "applicationId" field in the submit profile in the eas.json.'
);
return result(null);
}
return result(
new Error(`Failed to resolve applicationId in Android project: ${error.message}.`)
);
}
}
@@ -137,7 +120,7 @@ export default class AndroidSubmitCommand {
}
}
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
private async resolveServiceAccountSourceAsync(): Promise<Result<ServiceAccountSource>> {
const { serviceAccountKeyPath } = this.ctx.profile;
if (serviceAccountKeyPath) {
return result({
@@ -145,8 +128,22 @@ export default class AndroidSubmitCommand {
path: serviceAccountKeyPath,
});
}
let androidApplicationIdentifier: string | undefined =
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.applicationId;
if (!androidApplicationIdentifier) {
const androidApplicationIdentifierResult =
await this.maybeGetAndroidPackageFromCurrentProjectAsync();
if (!androidApplicationIdentifierResult.ok) {
return result(androidApplicationIdentifierResult.reason);
}
const androidApplicationIdentifierValue = androidApplicationIdentifierResult.enforceValue();
if (androidApplicationIdentifierValue) {
androidApplicationIdentifier = androidApplicationIdentifierValue;
}
}
return result({
sourceType: ServiceAccountSourceType.credentialsService,
androidApplicationIdentifier,
});
}
}
@@ -18,19 +18,18 @@ import {
formatArchiveSourceSummary,
printSummary,
} from '../utils/summary';
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
import {
ServiceAccountKeyResult,
ServiceAccountSource,
getServiceAccountKeyResultAsync,
} from './ServiceAccountSource';
export interface AndroidSubmissionOptions
extends Pick<
AndroidSubmissionConfigInput,
'track' | 'releaseStatus' | 'changesNotSentForReview'
> {
projectId: string;
androidPackageSource: AndroidPackageSource;
archiveSource: ArchiveSource;
serviceAccountSource: ServiceAccountSource;
}
@@ -51,12 +50,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
archive: async () => await getArchiveAsync(this.options.archiveSource),
// eslint-disable-next-line async-protect/async-suffix
serviceAccountKeyResult: async () => {
const androidPackage = await getAndroidPackageAsync(this.options.androidPackageSource);
return await getServiceAccountKeyResultAsync(
this.ctx,
this.options.serviceAccountSource,
androidPackage
);
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
},
};
const sourceOptionsAnalytics = {
@@ -6,6 +6,10 @@ import nullthrows from 'nullthrows';
import { SetUpGoogleServiceAccountKey } from '../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
import Log, { learnMore } from '../../log';
import {
INVALID_APPLICATION_ID_MESSAGE,
isApplicationIdValid,
} from '../../project/android/applicationId';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
@@ -32,6 +36,7 @@ interface ServiceAccountPromptSource extends ServiceAccountSourceBase {
export interface ServiceAccountCredentialsServiceSource extends ServiceAccountSourceBase {
sourceType: ServiceAccountSourceType.credentialsService;
androidApplicationIdentifier?: string;
}
export type ServiceAccountKeyResult = {
@@ -60,11 +65,10 @@ export type ServiceAccountSource =
export async function getServiceAccountKeyResultAsync(
ctx: SubmissionContext<Platform.ANDROID>,
source: ServiceAccountSource,
androidApplicationIdentifier: string
source: ServiceAccountSource
): Promise<ServiceAccountKeyResult> {
if (source.sourceType === ServiceAccountSourceType.credentialsService) {
return await getServiceAccountFromCredentialsServiceAsync(ctx, androidApplicationIdentifier);
return await getServiceAccountFromCredentialsServiceAsync(ctx, source);
} else {
return await getServiceAccountLocallyAsync(source);
}
@@ -97,9 +101,19 @@ export async function getServiceAccountKeyPathAsync(source: ServiceAccountSource
}
}
async function promptForApplicationIdAsync(): Promise<string> {
const { androidPackage } = await promptAsync({
name: 'androidPackage',
message: 'Android package name:',
type: 'text',
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
});
return androidPackage;
}
export async function getServiceAccountFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.ANDROID>,
androidApplicationIdentifier: string
source: ServiceAccountCredentialsServiceSource
): Promise<ServiceAccountKeyResult> {
const appLookupParams = {
account: nullthrows(
@@ -107,8 +121,12 @@ export async function getServiceAccountFromCredentialsServiceAsync(
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
androidApplicationIdentifier,
androidApplicationIdentifier:
source.androidApplicationIdentifier ?? (await promptForApplicationIdAsync()),
};
Log.log(
`Looking up credentials configuration for ${appLookupParams.androidApplicationIdentifier}...`
);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
ctx.credentialsCtx
@@ -152,7 +152,7 @@ describe(getServiceAccountKeyResultAsync, () => {
sourceType: ServiceAccountSourceType.path,
path: '/project_dir/subdir/service-account.json',
};
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
@@ -186,7 +186,7 @@ describe(getServiceAccountKeyResultAsync, () => {
const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.prompt,
};
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source, 'test.sdf');
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
googleServiceAccountKeyJson: mockDetectableServiceAccountJson,
@@ -214,13 +214,10 @@ describe(getServiceAccountKeyResultAsync, () => {
},
nonInteractive: true,
});
const serviceAccountResult = await getServiceAccountKeyResultAsync(
ctx,
{
sourceType: ServiceAccountSourceType.credentialsService,
},
'test.application.identiifer'
);
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, {
sourceType: ServiceAccountSourceType.credentialsService,
androidApplicationIdentifier: 'test.application.identiifer',
});
expect(serviceAccountResult).toMatchObject({
result: { googleServiceAccountKeyId: 'test-id' },
summary: { source: 'EAS servers', email: '[email protected]' },
+4 -1
View File
@@ -25,6 +25,7 @@ export interface SubmissionContext<T extends Platform> {
projectId: string;
projectName: string;
user: Actor;
applicationIdentifierOverride?: string;
}
export interface SubmitArchiveFlags {
@@ -43,8 +44,9 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
profile: SubmitProfile<T>;
projectDir: string;
projectId: string;
applicationIdentifier?: string;
}): Promise<SubmissionContext<T>> {
const { projectDir, nonInteractive } = params;
const { applicationIdentifier, projectDir, nonInteractive } = params;
const exp = getExpoConfig(projectDir, { env: params.env });
const { env, ...rest } = params;
const user = await ensureLoggedInAsync();
@@ -74,5 +76,6 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
projectName,
user,
trackingCtx,
applicationIdentifierOverride: applicationIdentifier,
};
}
@@ -3,7 +3,11 @@ import nullthrows from 'nullthrows';
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import {
INVALID_BUNDLE_IDENTIFIER_MESSAGE,
isBundleIdentifierValid,
} from '../../project/ios/bundleIdentifier';
import { promptAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import {
@@ -16,15 +20,29 @@ import { AscApiKeyResult } from './AscApiKeySource';
* The Credentials Service will either return an ASC API Key or an App Specific Password
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
*/
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE';
export type CredentialsServiceSource = typeof CREDENTIALS_SERVICE_SOURCE;
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE' as const;
export interface CredentialsServiceSource {
sourceType: typeof CREDENTIALS_SERVICE_SOURCE;
bundleIdentifier?: string;
}
async function promptForBundleIdentifierAsync(): Promise<string> {
const { bundleIdentifier } = await promptAsync({
name: 'bundleIdentifier',
message: 'Bundle identifier:',
type: 'text',
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
});
return bundleIdentifier;
}
export async function getFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
ctx: SubmissionContext<Platform.IOS>,
source: CredentialsServiceSource
): Promise<
{ appSpecificPassword: AppSpecificPasswordCredentials } | { ascApiKeyResult: AscApiKeyResult }
> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
const bundleIdentifier = source.bundleIdentifier ?? (await promptForBundleIdentifierAsync());
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
@@ -7,6 +7,10 @@ import wrapAnsi from 'wrap-ansi';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import {
AmbiguousBundleIdentifierError,
getBundleIdentifierAsync,
} from '../../project/ios/bundleIdentifier';
import { ArchiveSource } from '../ArchiveSource';
import { resolveArchiveSource } from '../commons';
import { SubmissionContext } from '../context';
@@ -29,33 +33,9 @@ export default class IosSubmitCommand {
return await submitter.submitAsync();
}
private resolveCredentialSubmissionOptions():
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
| { credentialsServiceSource: Result<CredentialsServiceSource> } {
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldSkipAscApiKeySource =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAscApiKeySource) {
return { ascApiKeySource };
}
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAppSpecificPasswordSource) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
}
return {
credentialsServiceSource: result(CREDENTIALS_SERVICE_SOURCE),
};
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
const archiveSource = this.resolveArchiveSource();
const credentialsSource = this.resolveCredentialSubmissionOptions();
const credentialsSource = await this.resolveCredentialSubmissionOptionsAsync();
const maybeAppSpecificPasswordSource =
'appSpecificPasswordSource' in credentialsSource
? credentialsSource.appSpecificPasswordSource
@@ -103,6 +83,64 @@ export default class IosSubmitCommand {
};
}
private async maybeGetIosBundleIdentifierAsync(): Promise<Result<string | null>> {
try {
return result(await getBundleIdentifierAsync(this.ctx.projectDir, this.ctx.exp));
} catch (error: any) {
if (error instanceof AmbiguousBundleIdentifierError) {
Log.warn(
'bundleIdentifier in the Xcode project is ambiguous, specify it via "bundleIdentifier" field in the submit profile in the eas.json.'
);
return result(null);
}
return result(
new Error(`Failed to resolve bundleIdentifier in the Xcode project: ${error.message}.`)
);
}
}
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
| { credentialsServiceSource: Result<CredentialsServiceSource> }
> {
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldSkipAscApiKeySource =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAscApiKeySource) {
return { ascApiKeySource };
}
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAppSpecificPasswordSource) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
}
let bundleIdentifier =
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.bundleIdentifier;
if (!bundleIdentifier) {
const bundleIdentifierResult = await this.maybeGetIosBundleIdentifierAsync();
if (!bundleIdentifierResult.ok) {
return {
credentialsServiceSource: result(bundleIdentifierResult.reason),
};
}
const bundleIdentifierValue = bundleIdentifierResult.enforceValue();
if (bundleIdentifierValue) {
bundleIdentifier = bundleIdentifierValue;
}
}
return {
credentialsServiceSource: result({
sourceType: CREDENTIALS_SERVICE_SOURCE,
bundleIdentifier,
}),
};
}
private resolveAppSpecificPasswordSource(): Result<AppSpecificPasswordSource> {
const envAppSpecificPassword = getenv.string('EXPO_APPLE_APP_SPECIFIC_PASSWORD', '');
@@ -68,7 +68,7 @@ export default class IosSubmitter extends BaseSubmitter<
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
: null;
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
? await getFromCredentialsServiceAsync(this.ctx)
? await getFromCredentialsServiceAsync(this.ctx, this.options.credentialsServiceSource)
: null;
return {
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
@@ -9,7 +9,10 @@ import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
import { getFromCredentialsServiceAsync } from '../CredentialsServiceSource';
import {
CREDENTIALS_SERVICE_SOURCE,
getFromCredentialsServiceAsync,
} from '../CredentialsServiceSource';
jest.mock('fs');
jest.mock('../../../prompts');
@@ -57,7 +60,10 @@ describe(getFromCredentialsServiceAsync, () => {
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
asMock(promptAsync).mockImplementationOnce(() => ({ appleId: '[email protected]' }));
const result = await getFromCredentialsServiceAsync(ctx);
const result = await getFromCredentialsServiceAsync(ctx, {
sourceType: CREDENTIALS_SERVICE_SOURCE,
bundleIdentifier: 'com.expo.test.project',
});
expect(result).toEqual({
appSpecificPassword: { password: 'super secret', appleIdUsername: '[email protected]' },
});
@@ -80,7 +86,10 @@ describe(getFromCredentialsServiceAsync, () => {
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getFromCredentialsServiceAsync(ctx);
const result = await getFromCredentialsServiceAsync(ctx, {
sourceType: CREDENTIALS_SERVICE_SOURCE,
bundleIdentifier: 'com.expo.test.project',
});
expect(result).toEqual({
ascApiKeyResult: {
result: {
@@ -1,90 +1,107 @@
import { Platform } from '@expo/eas-build-job';
import { errors } from '@expo/eas-json';
import { EasJsonReader, errors } from '@expo/eas-json';
import { getProfilesAsync } from '../profiles';
jest.mock('@expo/eas-json', () => {
const actual = jest.requireActual('@expo/eas-json');
const EasJsonReaderMock = jest.fn();
EasJsonReaderMock.prototype = {
getBuildProfileAsync: jest.fn(),
getSubmitProfileAsync: jest.fn(),
};
return {
...actual,
EasJsonReader: EasJsonReaderMock,
};
});
const getBuildProfileAsync = jest.spyOn(EasJsonReader.prototype, 'getBuildProfileAsync');
describe(getProfilesAsync, () => {
afterEach(() => {
getBuildProfileAsync.mockReset();
});
it('defaults to production profile', async () => {
const readProfileAsync = jest.fn();
const result = await getProfilesAsync({
projectDir: '/fake',
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
type: 'build',
});
expect(result[0].profileName).toBe('production');
expect(result[1].profileName).toBe('production');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
});
it('defaults to release profile when production profile is non-existent', async () => {
const readProfileAsync = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
getBuildProfileAsync
.mockImplementationOnce(() => {
throw new errors.MissingProfileError();
})
.mockRejectedValueOnce(() => {
throw new Error();
.mockImplementationOnce(() => {
throw new errors.MissingProfileError();
});
const result = await getProfilesAsync({
projectDir: '/fake',
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
type: 'build',
});
expect(result[0].profileName).toBe('release');
expect(result[1].profileName).toBe('release');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'production');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'release');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'release');
});
it('fails when neither production or release profiles are present', async () => {
const readProfileAsync = jest
.fn()
.mockRejectedValueOnce(() => {
throw new Error();
})
.mockRejectedValueOnce(() => {
throw new Error();
});
getBuildProfileAsync.mockImplementation(() => {
throw new errors.MissingProfileError();
});
await expect(
getProfilesAsync({
projectDir: '/fake',
platforms: [Platform.ANDROID],
profileName: undefined,
readProfileAsync,
type: 'build',
})
).rejects.toThrowError(/There is no profile named "production" in eas.json/);
).rejects.toThrowError(errors.MissingProfileError);
});
it('gets a specific profile', async () => {
const readProfileAsync = jest.fn();
const result = await getProfilesAsync({
projectDir: '/fake',
platforms: [Platform.ANDROID, Platform.IOS],
profileName: 'custom-profile',
readProfileAsync,
type: 'build',
});
expect(result[0].profileName).toBe('custom-profile');
expect(result[1].profileName).toBe('custom-profile');
expect(readProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
expect(readProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
});
it('throws validation error if eas.json is invalid', async () => {
const readProfileAsync = jest.fn().mockImplementation(() => {
getBuildProfileAsync.mockImplementation(() => {
throw new errors.InvalidEasJsonError('eas.json is not valid');
});
await expect(
getProfilesAsync({
projectDir: '/fake',
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
readProfileAsync,
type: 'build',
})
).rejects.toThrowError(/eas.json is not valid/);
});
@@ -3,12 +3,17 @@ import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
export async function readPlistAsync(plistPath: string): Promise<object> {
export async function readPlistAsync(plistPath: string): Promise<object | null> {
if (await fs.pathExists(plistPath)) {
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
return plist.parse(expoPlistContent);
try {
return plist.parse(expoPlistContent);
} catch (err: any) {
err.message = `Failed to parse ${plistPath}. ${err.message}`;
throw err;
}
} else {
return {};
return null;
}
}
+65 -15
View File
@@ -1,51 +1,68 @@
import { Platform } from '@expo/eas-build-job';
import { errors } from '@expo/eas-json';
import {
BuildProfile,
EasJsonReader,
ProfileType,
SubmitProfile,
errors,
getDefaultSubmitProfile,
} from '@expo/eas-json';
import Log from '../log';
export type ProfileData<T> = {
profile: T;
type EasProfile<T extends ProfileType> = T extends 'build'
? BuildProfile<Platform>
: SubmitProfile<Platform>;
export type ProfileData<T extends ProfileType> = {
profile: EasProfile<T>;
platform: Platform;
profileName: string;
};
export async function getProfilesAsync<T>({
export async function getProfilesAsync<T extends ProfileType>({
projectDir,
platforms,
profileName: profileNameArg,
// eslint-disable-next-line async-protect/async-suffix
readProfileAsync,
type,
}: {
projectDir: string;
platforms: Platform[];
profileName?: string | null;
readProfileAsync: (platform: Platform, profileName: string) => Promise<T>;
profileName?: string;
type: T;
}): Promise<ProfileData<T>[]> {
const results = platforms.map(async function (platform) {
let profile;
let profile: EasProfile<T>;
let profileName = profileNameArg;
if (!profileName) {
try {
profile = await readProfileAsync(platform, 'production');
profileName = 'production';
profile = await readProfileAsync({ projectDir, platform, type, profileName });
} catch (errorOuter) {
if (errorOuter instanceof errors.InvalidEasJsonError) {
if (!(errorOuter instanceof errors.MissingProfileError)) {
throw errorOuter;
}
try {
profile = await readProfileAsync(platform, 'release');
profileName = 'release';
profile = await readProfileAsync({ projectDir, platform, type, profileName });
Log.warn(
'The default profile changed from "release" to "production". We detected that you still have a "release" build profile, so we are using it. Update eas.json to have a profile named "production" under the `build` key, or specify which profile you\'d like to use with the --profile flag. This fallback behavior will be removed in the next major version of EAS CLI.'
);
} catch (errorInner) {
if (errorInner instanceof errors.InvalidEasJsonError) {
if (!(errorInner instanceof errors.MissingProfileError)) {
throw errorInner;
}
throw new Error('There is no profile named "production" in eas.json');
const defaultProfile = getDefaultProfile({ platform, type });
if (!defaultProfile) {
throw errorInner;
}
profileName = '__default__';
profile = defaultProfile;
}
}
} else {
profile = await readProfileAsync(platform, profileName);
profile = await readProfileAsync({ projectDir, platform, type, profileName });
}
return {
@@ -57,3 +74,36 @@ export async function getProfilesAsync<T>({
return await Promise.all(results);
}
async function readProfileAsync<T extends ProfileType>({
projectDir,
platform,
type,
profileName,
}: {
projectDir: string;
platform: Platform;
type: T;
profileName: string;
}): Promise<EasProfile<T>> {
const easJsonReader = new EasJsonReader(projectDir);
if (type === 'build') {
return (await easJsonReader.getBuildProfileAsync(platform, profileName)) as EasProfile<T>;
} else {
return (await easJsonReader.getSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
}
}
function getDefaultProfile<T extends ProfileType>({
platform,
type,
}: {
platform: Platform;
type: T;
}): EasProfile<T> | null {
if (type === 'build') {
return null;
} else {
return getDefaultSubmitProfile(platform) as EasProfile<T>;
}
}
+98 -6
View File
@@ -1,6 +1,9 @@
import spawnAsync from '@expo/spawn-async';
import { exit } from '@oclif/errors';
import chalk from 'chalk';
import Log, { learnMore } from '../../log';
import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts';
import {
doesGitRepoExistAsync,
@@ -14,7 +17,13 @@ import { Client } from '../vcs';
export default class GitClient extends Client {
public async ensureRepoExistsAsync(): Promise<void> {
if (!(await isGitInstalledAsync())) {
throw new Error('git command not found, install it before proceeding');
Log.error(
`${chalk.bold('git')} command not found. Install it before proceeding or set ${chalk.bold(
'EAS_NO_VCS=1'
)} to use EAS CLI without Git (or any other version control system).`
);
Log.error(learnMore('https://expo.fyi/eas-vcs-workflow'));
exit(1);
}
if (await doesGitRepoExistAsync()) {
@@ -49,16 +58,29 @@ export default class GitClient extends Client {
public async commitAsync({
commitMessage,
commitAllFiles,
nonInteractive = false,
}: {
commitMessage: string;
commitAllFiles?: boolean;
nonInteractive?: boolean;
}): Promise<void> {
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A']);
}
await ensureGitConfiguredAsync({ nonInteractive });
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', commitMessage]);
try {
if (commitAllFiles) {
await spawnAsync('git', ['add', '-A']);
}
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', commitMessage]);
} catch (err: any) {
if (err?.stdout) {
Log.error(err.stdout);
}
if (err?.stderr) {
Log.error(err.stderr);
}
throw err;
}
}
public async isCommitRequiredAsync(): Promise<boolean> {
@@ -171,6 +193,76 @@ export default class GitClient extends Client {
}
}
async function ensureGitConfiguredAsync({
nonInteractive,
}: {
nonInteractive: boolean;
}): Promise<void> {
let usernameConfigured = true;
let emailConfigured = true;
try {
await spawnAsync('git', ['config', '--get', 'user.name']);
} catch (err: any) {
Log.debug(err);
usernameConfigured = false;
}
try {
await spawnAsync('git', ['config', '--get', 'user.email']);
} catch (err: any) {
Log.debug(err);
emailConfigured = false;
}
if (usernameConfigured && emailConfigured) {
return;
}
Log.warn(
`You need to configure Git with your ${[
!usernameConfigured && 'username (user.name)',
!emailConfigured && 'email address (user.email)',
]
.filter(i => i)
.join(' and ')}`
);
if (nonInteractive) {
throw new Error('Git cannot be configured automatically in non-interactive mode');
}
if (!usernameConfigured) {
const { username } = await promptAsync({
type: 'text',
name: 'username',
message: 'Username:',
validate: (input: string) => input !== '',
});
const spinner = ora(
`Running ${chalk.bold(`git config --local user.name ${username}`)}`
).start();
try {
await spawnAsync('git', ['config', '--local', 'user.name', username]);
spinner.succeed();
} catch (err: any) {
spinner.fail();
throw err;
}
}
if (!emailConfigured) {
const { email } = await promptAsync({
type: 'text',
name: 'email',
message: 'Email address:',
validate: (input: string) => input !== '',
});
const spinner = ora(`Running ${chalk.bold(`git config --local user.email ${email}`)}`).start();
try {
await spawnAsync('git', ['config', '--local', 'user.email', email]);
spinner.succeed();
} catch (err: any) {
spinner.fail();
throw err;
}
}
}
/**
* Checks if git is configured to be case sensitive
* @returns {boolean | undefined}
+4 -3
View File
@@ -34,9 +34,10 @@ export abstract class Client {
// - Should be implemented if hasUncommittedChangesAsync is implemented
// - If it's not implemented method `makeShallowCopyAsync` needs to be able to include uncommitted changes
// in project copy
public async commitAsync(arg: {
public async commitAsync(_arg: {
commitMessage: string;
commitAllFiles?: boolean;
nonInteractive?: boolean;
}): Promise<void> {
// it should not be called unless hasUncommittedChangesAsync is implemented
throw new Error('commitAsync is not implemented');
@@ -44,7 +45,7 @@ export abstract class Client {
// (optional) mark file as tracked, if this method is called on file, the next call to
// `commitAsync({ commitAllFiles: false })` should commit that file
public async trackFileAsync(file: string): Promise<void> {}
public async trackFileAsync(_file: string): Promise<void> {}
// (optional) print diff of the changes that will be commited in the next call to
// `commitAsync({ commitAllFiles: false })`
@@ -74,7 +75,7 @@ export abstract class Client {
//
// @param filePath has to be a relative normalized path pointing to a file
// located under the root of the repository
public async isFileIgnoredAsync(filePath: string): Promise<boolean> {
public async isFileIgnoredAsync(_filePath: string): Promise<boolean> {
return false;
}
}
+20 -3
View File
@@ -7,19 +7,36 @@ import { promptAsync } from '../prompts';
export async function prepareInputParamsAsync(
{
event,
event: maybeEvent,
url: maybeUrl,
secret: maybeSecret,
}: {
event: WebhookType;
event?: WebhookType;
url?: string;
secret?: string;
},
existingWebhook?: WebhookFragment
): Promise<WebhookInput> {
let event: WebhookType | undefined = maybeEvent;
let url: string | undefined = maybeUrl;
let secret: string | undefined = maybeSecret;
if (!event) {
const choices = [
{ title: 'Build', value: WebhookType.Build },
{ title: 'Submit', value: WebhookType.Submit },
];
({ event } = await promptAsync({
type: 'select',
name: 'event',
message: 'Webhook event type:',
choices,
initial: existingWebhook?.event
? choices.findIndex(choice => choice.value === existingWebhook.event)
: undefined,
}));
}
if (!url || !validateURL(url)) {
const urlValidationMessage =
'The provided webhook URL is invalid and must be an absolute URL, including a scheme.';
@@ -50,7 +67,7 @@ export async function prepareInputParamsAsync(
}
return {
event,
event: nullthrows(event),
url: nullthrows(url),
secret: nullthrows(secret),
};
-1
View File
@@ -1 +0,0 @@
/build
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "0.36.0",
"version": "0.38.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
-31
View File
@@ -1,31 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidBuildProfile, CommonBuildProfile, IosBuildProfile } from './EasBuild.types';
import { AndroidSubmitProfile, IosSubmitProfile } from './EasSubmit.types';
export enum CredentialsSource {
LOCAL = 'local',
REMOTE = 'remote',
}
export interface RawBuildProfile extends Partial<CommonBuildProfile> {
extends?: string;
[Platform.ANDROID]?: Partial<AndroidBuildProfile>;
[Platform.IOS]?: Partial<IosBuildProfile>;
}
export interface EasSubmitConfiguration {
[Platform.ANDROID]?: AndroidSubmitProfile;
[Platform.IOS]?: IosSubmitProfile;
}
export interface CliConfig {
version?: string;
requireCommit?: boolean;
}
export interface EasJson {
cli?: CliConfig;
build: { [profile: string]: RawBuildProfile };
submit?: { [profile: string]: EasSubmitConfiguration };
}
-259
View File
@@ -1,259 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import envString from 'env-string';
import path from 'path';
import { BuildProfile } from './EasBuild.types';
import { CliConfig, CredentialsSource, EasJson, RawBuildProfile } from './EasJson.types';
import {
AndroidSubmitProfileSchema,
CliConfigSchema,
EasJsonSchema,
IosSubmitProfileSchema,
MinimalEasJsonSchema,
} from './EasJsonSchema';
import {
AndroidSubmitProfileFieldsToEvaluate,
IosSubmitProfileFieldsToEvaluate,
SubmitProfile,
} from './EasSubmit.types';
import { InvalidEasJsonError } from './errors';
interface EasJsonPreValidation {
cli?: object;
build: { [profile: string]: object };
submit?: { [profile: string]: object };
}
const defaults = {
distribution: 'store',
credentialsSource: CredentialsSource.REMOTE,
} as const;
type LoggerFn = (...args: any[]) => void;
interface Logger {
log: LoggerFn;
warn: LoggerFn;
}
export class EasJsonReader {
private static log?: Logger;
public static formatEasJsonPath(projectDir: string): string {
return path.join(projectDir, 'eas.json');
}
public static setLog(log: Logger): void {
this.log = log;
}
constructor(private projectDir: string) {}
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readRawAsync();
return Object.keys(easJson?.build ?? {});
}
public async getCliConfigAsync(): Promise<CliConfig | null> {
try {
const easJson = await this.readRawAsync();
if (!easJson.cli) {
return null;
}
const { value, error } = CliConfigSchema.validate(easJson.cli, {
allowUnknown: false,
convert: true,
abortEarly: false,
});
if (error) {
throw new Error(`"cli" field in eas.json is not valid [${error.toString()}]`);
}
return value as CliConfig;
} catch (err: any) {
if (err.code === 'ENOENT') {
return null;
}
throw err;
}
}
public async getSubmitProfileNamesAsync({ throwIfEasJsonDoesNotExist = true } = {}): Promise<
string[]
> {
try {
const easJson = await this.readRawAsync();
return Object.keys(easJson?.submit ?? {});
} catch (err: any) {
if (!throwIfEasJsonDoesNotExist && err.code === 'ENOENT') {
return [];
} else {
throw err;
}
}
}
public async readBuildProfileAsync<T extends Platform>(
platform: T,
profileName: string
): Promise<BuildProfile<T>> {
const easJson = await this.readAndValidateAsync();
this.ensureBuildProfileExists(easJson, profileName);
const {
android: resolvedAndroidSpecificValues,
ios: resolvedIosSpecificValues,
...resolvedProfile
} = this.resolveBuildProfile(easJson, profileName);
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 {
throw new Error(`Unknown platform ${platform}`);
}
}
public async readSubmitProfileAsync<T extends Platform>(
platform: T,
profileNameArg?: string
): Promise<SubmitProfile<T>> {
let profileName = profileNameArg;
if (!profileName) {
const profileNames = await this.getSubmitProfileNamesAsync({
throwIfEasJsonDoesNotExist: false,
});
if (profileNames.includes('production')) {
profileName = 'production';
} else if (profileNames.includes('release')) {
profileName = 'release';
} else {
return getDefaultSubmitProfile(platform);
}
}
const easJson = await this.readAndValidateAsync();
const profile = easJson?.submit?.[profileName];
if (!profile) {
throw new Error(`There is no profile named ${profileName} in eas.json`);
}
const platformProfile = profile[platform];
if (platformProfile) {
return this.evaluateFields(platform, platformProfile as SubmitProfile<T>);
} else {
return getDefaultSubmitProfile(platform);
}
}
public async readAndValidateAsync(): Promise<EasJson> {
const easJson = await this.readRawAsync();
const { value, error } = EasJsonSchema.validate(easJson, {
allowUnknown: false,
convert: true,
abortEarly: false,
});
if (error) {
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
}
return value as EasJson;
}
public async readRawAsync(): Promise<EasJsonPreValidation> {
try {
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
const rawEasJson = JsonFile.read(easJsonPath);
const { value, error } = MinimalEasJsonSchema.validate(rawEasJson, { abortEarly: false });
if (error) {
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
}
return value;
} catch (err: any) {
if (err.code === 'EJSONPARSE') {
err.message = `Found invalid JSON in eas.json. ${err.message}`;
}
throw err;
}
}
private resolveBuildProfile(
easJson: EasJson,
profileName: string,
depth: number = 0
): 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 = easJson.build[profileName];
if (!buildProfile) {
throw new Error(`There is no profile named ${profileName} in eas.json`);
}
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
if (baseProfileName) {
return profileMerge(
this.resolveBuildProfile(easJson, baseProfileName, depth + 1),
buildProfileRest
);
} else {
return buildProfileRest;
}
}
private ensureBuildProfileExists(easJson: EasJson, profileName: string): void {
if (!easJson.build || !easJson.build[profileName]) {
throw new Error(`There is no profile named ${profileName} in eas.json`);
}
}
private evaluateFields<T extends Platform>(
platform: T,
profile: SubmitProfile<T>
): SubmitProfile<T> {
const fields =
platform === Platform.ANDROID
? AndroidSubmitProfileFieldsToEvaluate
: IosSubmitProfileFieldsToEvaluate;
const evaluatedProfile = { ...profile };
for (const field of fields) {
if (field in evaluatedProfile) {
// @ts-ignore
evaluatedProfile[field] = envString(evaluatedProfile[field], process.env);
}
}
return evaluatedProfile;
}
}
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;
}
function getDefaultSubmitProfile<T extends Platform>(platform: T): SubmitProfile<T> {
const Schema =
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
return Schema.validate({}, { convert: true }).value;
}
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { EasJsonReader } from '../EasJsonReader';
import { EasJsonReader } from '../reader';
jest.mock('fs');
@@ -19,18 +19,18 @@ test('minimal valid eas.json for both platforms', async () => {
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'production');
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'production');
expect({
expect(androidProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(androidProfile);
});
expect({
expect(iosProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(iosProfile);
});
});
test('valid eas.json for development client builds', async () => {
@@ -47,20 +47,20 @@ test('valid eas.json for development client builds', async () => {
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'debug');
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
expect({
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'debug');
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
}).toEqual(androidProfile);
});
expect({
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
}).toEqual(iosProfile);
});
});
test('valid profile for internal distribution on Android', async () => {
@@ -73,11 +73,11 @@ test('valid profile for internal distribution on Android', async () => {
});
const reader = new EasJsonReader('/project');
const profile = await reader.readBuildProfileAsync(Platform.ANDROID, 'internal');
expect({
const profile = await reader.getBuildProfileAsync(Platform.ANDROID, 'internal');
expect(profile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
}).toEqual(profile);
});
});
test('valid profile extending other profile', async () => {
@@ -95,18 +95,18 @@ test('valid profile extending other profile', async () => {
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
const extendedProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
expect({
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
node: '12.0.0',
}).toEqual(baseProfile);
expect({
});
expect(extendedProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
node: '13.0.0',
}).toEqual(extendedProfile);
});
});
test('valid profile extending other profile with platform specific envs', async () => {
@@ -134,33 +134,33 @@ test('valid profile extending other profile with platform specific envs', async
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
expect({
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'base',
},
}).toEqual(baseProfile);
expect({
});
expect(extendedAndroidProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension:android',
},
}).toEqual(extendedAndroidProfile);
expect({
});
expect(extendedIosProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension',
},
}).toEqual(extendedIosProfile);
});
});
test('valid profile extending other profile with platform specific caching', async () => {
@@ -188,32 +188,32 @@ test('valid profile extending other profile with platform specific caching', asy
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
expect({
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
cache: {
disabled: true,
},
}).toEqual(baseProfile);
expect({
});
expect(extendedAndroidProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
}).toEqual(extendedAndroidProfile);
expect({
});
expect(extendedIosProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
cache: {
key: 'extend-key',
},
}).toEqual(extendedIosProfile);
});
});
test('valid eas.json with missing profile', async () => {
@@ -224,8 +224,8 @@ test('valid eas.json with missing profile', async () => {
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
await expect(promise).rejects.toThrowError('There is no profile named debug in eas.json');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
});
test('invalid eas.json when using wrong buildType', async () => {
@@ -236,7 +236,7 @@ test('invalid eas.json when using wrong buildType', async () => {
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.production.android.buildType" must be one of [apk, app-bundle]]'
);
@@ -246,8 +246,8 @@ test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError('There is no profile named production in eas.json');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
});
test('invalid semver value', async () => {
@@ -258,17 +258,29 @@ test('invalid semver value', async () => {
});
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.production.node" failed custom validation because 12.0.0-alpha is not a valid version]'
);
});
test('invalid release channel', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: { releaseChannel: 'feature/myfeature' },
},
});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
});
test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
production: { node: '12.0.0' },
blah: { node: '12.0.0' },
},
});
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { EasJsonReader } from '../EasJsonReader';
import { EasJsonReader } from '../reader';
jest.mock('fs');
@@ -19,8 +19,8 @@ test('minimal allowed eas.json for both platforms', async () => {
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
changesNotSentForReview: false,
@@ -46,7 +46,7 @@ test('android config with all required values', async () => {
});
const reader = new EasJsonReader('/project');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -72,7 +72,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
try {
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
const reader = new EasJsonReader('/project');
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -102,7 +102,7 @@ test('ios config with all required values', async () => {
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
expect(iosProfile).toEqual({
appleId: '[email protected]',
@@ -136,7 +136,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'release');
expect(iosProfile).toEqual({
appleId: '[email protected]',
@@ -153,3 +153,45 @@ test('ios config with ascApiKey fields set to env var', async () => {
process.env.ASC_API_KEY_ID = undefined;
}
});
test('valid profile extending other profile', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
base: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
},
},
extension: {
extends: 'base',
ios: {
appleTeamId: 'ABCDEF',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
},
},
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'base');
const extendedProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'extension');
expect(baseProfile).toEqual({
language: 'en-US',
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
});
expect(extendedProfile).toEqual({
language: 'en-US',
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'ABCDEF',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
});
});
+97
View File
@@ -0,0 +1,97 @@
import { Platform } from '@expo/eas-build-job';
import { MissingParentProfileError, MissingProfileError } from '../errors';
import { EasJson } from '../types';
import { BuildProfileSchema } from './schema';
import { BuildProfile, EasJsonBuildProfile } from './types';
type EasJsonBuildProfileResolved = Omit<EasJsonBuildProfile, 'extends'>;
export function resolveBuildProfile<T extends Platform>({
easJson,
platform,
profileName,
}: {
easJson: EasJson;
platform: T;
profileName: string;
}): BuildProfile<T> {
const easJsonProfile = resolveProfile({
easJson,
profileName,
});
const { android, ios, ...base } = easJsonProfile;
const withoutDefaults = mergeProfiles(base, easJsonProfile[platform] ?? {});
return mergeProfiles(getDefaultProfile(platform), withoutDefaults) as BuildProfile<T>;
}
function resolveProfile({
easJson,
profileName,
depth = 0,
}: {
easJson: EasJson;
profileName: string;
depth?: number;
}): EasJsonBuildProfileResolved {
if (depth >= 2) {
throw new Error(
'Too long chain of profile extensions, make sure "extends" keys do not make a cycle'
);
}
const profile = easJson.build?.[profileName];
if (!profile) {
if (depth === 0) {
throw new MissingProfileError(`Missing build profile in eas.json: ${profileName}`);
} else {
throw new MissingParentProfileError(
`Extending non-existent build profile in eas.json: ${profileName}`
);
}
}
const { extends: baseProfileName, ...rest } = profile;
if (baseProfileName) {
const baseProfile = resolveProfile({
easJson,
profileName: baseProfileName,
depth: depth + 1,
});
return mergeProfiles(baseProfile, rest);
} else {
return rest;
}
}
function mergeProfiles(
base: EasJsonBuildProfileResolved,
update: EasJsonBuildProfileResolved
): EasJsonBuildProfileResolved {
const result = {
...base,
...update,
};
if (base.env && update.env) {
result.env = {
...base.env,
...update.env,
};
}
if (base.android && update.android) {
result.android = mergeProfiles(base.android, update.android);
}
if (base.ios && update.ios) {
result.ios = mergeProfiles(base.ios, update.ios);
}
return result;
}
function getDefaultProfile<T extends Platform>(platform: T): EasJsonBuildProfile {
const defaultProfile = BuildProfileSchema.validate(
{},
{ allowUnknown: false, abortEarly: false, convert: true }
).value;
const { android, ios, ...base } = defaultProfile;
return mergeProfiles(base, defaultProfile[platform]);
}
@@ -1,16 +1,6 @@
import { Android, Ios } from '@expo/eas-build-job';
import Joi from 'joi';
import { AndroidReleaseStatus, AndroidReleaseTrack } from './EasSubmit.types';
const semverSchemaCheck = (value: any): any => {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
return value;
} else {
throw new Error(`${value} is not a valid version`);
}
};
const CacheSchema = Joi.object({
disabled: Joi.boolean(),
key: Joi.string().max(128),
@@ -19,11 +9,11 @@ const CacheSchema = Joi.object({
});
const CommonBuildProfileSchema = Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema,
releaseChannel: Joi.string(),
channel: Joi.string(),
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
developmentClient: Joi.boolean(),
node: Joi.string().empty(null).custom(semverSchemaCheck),
@@ -34,6 +24,8 @@ const CommonBuildProfileSchema = Joi.object({
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
withoutCredentials: Joi.boolean(),
image: Joi.string().valid(...Android.builderBaseImages),
@@ -52,6 +44,8 @@ const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
@@ -70,7 +64,7 @@ const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
})
);
const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
export const BuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
extends: Joi.string(),
android: AndroidBuildProfileSchema,
@@ -78,48 +72,10 @@ const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
})
);
export const AndroidSubmitProfileSchema = Joi.object({
serviceAccountKeyPath: Joi.string(),
track: Joi.string()
.valid(...Object.values(AndroidReleaseTrack))
.default(AndroidReleaseTrack.internal),
releaseStatus: Joi.string()
.valid(...Object.values(AndroidReleaseStatus))
.default(AndroidReleaseStatus.completed),
changesNotSentForReview: Joi.boolean().default(false),
});
export const IosSubmitProfileSchema = Joi.object({
ascApiKeyPath: Joi.string(),
ascApiKeyId: Joi.string(),
ascApiKeyIssuerId: Joi.string(),
appleId: Joi.string(),
ascAppId: Joi.string(),
appleTeamId: Joi.string(),
sku: Joi.string(),
language: Joi.string().default('en-US'),
companyName: Joi.string(),
appName: Joi.string(),
});
const EasJsonSubmitConfigurationSchema = Joi.object({
android: AndroidSubmitProfileSchema,
ios: IosSubmitProfileSchema,
});
export const CliConfigSchema = Joi.object({
version: Joi.string(),
requireCommit: Joi.boolean(),
});
export const MinimalEasJsonSchema = Joi.object({
cli: Joi.object(),
build: Joi.object().pattern(Joi.string(), Joi.object()),
submit: Joi.object().pattern(Joi.string(), Joi.object()),
});
export const EasJsonSchema = Joi.object({
cli: CliConfigSchema,
build: Joi.object().pattern(Joi.string(), EasJsonBuildProfileSchema),
submit: Joi.object().pattern(Joi.string(), EasJsonSubmitConfigurationSchema),
});
function semverSchemaCheck(value: any): any {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
return value;
} else {
throw new Error(`${value} is not a valid version`);
}
}
@@ -55,8 +55,10 @@ export interface IosBuildProfile extends CommonBuildProfile {
export type BuildProfile<TPlatform extends Platform = Platform> = TPlatform extends Platform.ANDROID
? AndroidBuildProfile
: TPlatform extends Platform.IOS
? IosBuildProfile
: TPlatform extends Platform
? AndroidBuildProfile | IosBuildProfile
: never;
: IosBuildProfile;
export interface EasJsonBuildProfile extends Partial<CommonBuildProfile> {
extends?: string;
[Platform.ANDROID]?: Partial<AndroidBuildProfile>;
[Platform.IOS]?: Partial<IosBuildProfile>;
}
+6
View File
@@ -1 +1,7 @@
export class InvalidEasJsonError extends Error {}
export class MissingEasJsonError extends Error {}
export class MissingProfileError extends Error {}
export class MissingParentProfileError extends Error {}
+6 -13
View File
@@ -1,20 +1,13 @@
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
export { EasJson, ProfileType } from './types';
export {
AndroidSubmitProfile,
AndroidReleaseStatus,
AndroidReleaseTrack,
IosSubmitProfile,
SubmitProfile,
} from './EasSubmit.types';
export { EasJson } from './EasJson.types';
export {
AndroidBuildProfile,
AndroidVersionAutoIncrement,
BuildProfile,
CredentialsSource,
DistributionType,
IosBuildProfile,
IosEnterpriseProvisioning,
IosVersionAutoIncrement,
AndroidVersionAutoIncrement,
} from './EasBuild.types';
export { EasJsonReader } from './EasJsonReader';
} from './build/types';
export { EasJsonReader } from './reader';
export * as errors from './errors';
+87
View File
@@ -0,0 +1,87 @@
import { Platform } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import fs from 'fs-extra';
import path from 'path';
import { resolveBuildProfile } from './build/resolver';
import { BuildProfile } from './build/types';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { EasJsonSchema } from './schema';
import { resolveSubmitProfile } from './submit/resolver';
import { SubmitProfile } from './submit/types';
import { EasJson } from './types';
export class EasJsonReader {
private easJson: EasJson | undefined;
constructor(private projectDir: string) {}
public static formatEasJsonPath(projectDir: string): string {
return path.join(projectDir, 'eas.json');
}
public async readAsync(): Promise<EasJson> {
if (this.easJson) {
return this.easJson;
}
try {
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
if (!(await fs.pathExists(easJsonPath))) {
throw new MissingEasJsonError(
`eas.json could not be found at ${easJsonPath}. Learn more at https://expo.fyi/eas-json`
);
}
const contents = JsonFile.read(easJsonPath);
const { value, error } = EasJsonSchema.validate(contents, {
allowUnknown: false,
abortEarly: false,
convert: true,
noDefaults: true,
});
if (error) {
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
}
this.easJson = value;
return value;
} catch (err: any) {
if (err.code === 'EJSONPARSE') {
err.message = `Found invalid JSON in eas.json. ${err.message}`;
}
throw err;
}
}
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readAsync();
return Object.keys(easJson?.build ?? {});
}
public async getBuildProfileAsync<T extends Platform>(
platform: T,
profileName: string
): Promise<BuildProfile<T>> {
const easJson = await this.readAsync();
return resolveBuildProfile({ easJson, platform, profileName });
}
public async getCliConfigAsync(): Promise<EasJson['cli'] | null> {
try {
const easJson = await this.readAsync();
return easJson.cli ?? null;
} catch (err: any) {
if (err instanceof MissingEasJsonError) {
return null;
}
throw err;
}
}
public async getSubmitProfileAsync<T extends Platform>(
platform: T,
profileName: string
): Promise<SubmitProfile<T>> {
const easJson = await this.readAsync();
return resolveSubmitProfile({ easJson, platform, profileName });
}
}
+13
View File
@@ -0,0 +1,13 @@
import Joi from 'joi';
import { BuildProfileSchema } from './build/schema';
import { SubmitProfileSchema } from './submit/schema';
export const EasJsonSchema = Joi.object({
cli: Joi.object({
version: Joi.string(),
requireCommit: Joi.boolean(),
}),
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
});
+114
View File
@@ -0,0 +1,114 @@
import { Platform } from '@expo/eas-build-job';
import envString from 'env-string';
import { MissingParentProfileError, MissingProfileError } from '../errors';
import { EasJson } from '../types';
import { AndroidSubmitProfileSchema, IosSubmitProfileSchema } from './schema';
import {
AndroidSubmitProfileFieldsToEvaluate,
IosSubmitProfileFieldsToEvaluate,
SubmitProfile,
} from './types';
export function resolveSubmitProfile<T extends Platform>({
easJson,
platform,
profileName,
}: {
easJson: EasJson;
platform: T;
profileName: string;
}): SubmitProfile<T> {
const submitProfile = resolveProfile({
easJson,
platform,
profileName,
});
const unevaluatedProfile = mergeProfiles(getDefaultProfile(platform), submitProfile);
return evaluateFields(platform, unevaluatedProfile);
}
function resolveProfile<T extends Platform>({
easJson,
profileName,
depth = 0,
platform,
}: {
platform: T;
easJson: EasJson;
profileName: string;
depth?: number;
}): SubmitProfile<T> | undefined {
if (depth >= 2) {
throw new Error(
'Too long chain of profile extensions, make sure "extends" keys do not make a cycle'
);
}
const profile = easJson.submit?.[profileName];
if (!profile) {
if (depth === 0) {
throw new MissingProfileError(`There is no submit profile named ${profileName} in eas.json`);
} else {
throw new MissingParentProfileError(
`Extending non-existent submit profile in eas.json: ${profileName}`
);
}
}
const { extends: baseProfileName, ...rest } = profile;
const platformProfile = rest[platform] as SubmitProfile<T> | undefined;
if (baseProfileName) {
const baseProfile = resolveProfile({
easJson,
platform,
profileName: baseProfileName,
depth: depth + 1,
});
return mergeProfiles(baseProfile, platformProfile);
} else {
return platformProfile;
}
}
function mergeProfiles<T extends Platform>(
base: SubmitProfile<T>,
update?: SubmitProfile<T>
): SubmitProfile<T>;
function mergeProfiles<T extends Platform>(
base?: SubmitProfile<T>,
update?: SubmitProfile<T>
): SubmitProfile<T> | undefined;
function mergeProfiles<T extends Platform>(
base?: SubmitProfile<T>,
update?: SubmitProfile<T>
): SubmitProfile<T> | undefined {
if (!update) {
return base;
}
return { ...base, ...update };
}
export function getDefaultProfile<T extends Platform>(platform: T): SubmitProfile<T> {
const Schema =
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
return Schema.validate({}, { allowUnknown: false, abortEarly: false, convert: true }).value;
}
function evaluateFields<T extends Platform>(
platform: T,
profile: SubmitProfile<T>
): SubmitProfile<T> {
const fields =
platform === Platform.ANDROID
? AndroidSubmitProfileFieldsToEvaluate
: IosSubmitProfileFieldsToEvaluate;
const evaluatedProfile = { ...profile };
for (const field of fields) {
if (field in evaluatedProfile) {
// @ts-ignore
evaluatedProfile[field] = envString(evaluatedProfile[field], process.env);
}
}
return evaluatedProfile;
}
+35
View File
@@ -0,0 +1,35 @@
import Joi from 'joi';
import { AndroidReleaseStatus, AndroidReleaseTrack } from './types';
export const AndroidSubmitProfileSchema = Joi.object({
serviceAccountKeyPath: Joi.string(),
track: Joi.string()
.valid(...Object.values(AndroidReleaseTrack))
.default(AndroidReleaseTrack.internal),
releaseStatus: Joi.string()
.valid(...Object.values(AndroidReleaseStatus))
.default(AndroidReleaseStatus.completed),
changesNotSentForReview: Joi.boolean().default(false),
applicationId: Joi.string(),
});
export const IosSubmitProfileSchema = Joi.object({
ascApiKeyPath: Joi.string(),
ascApiKeyId: Joi.string(),
ascApiKeyIssuerId: Joi.string(),
appleId: Joi.string(),
ascAppId: Joi.string(),
appleTeamId: Joi.string(),
sku: Joi.string(),
language: Joi.string().default('en-US'),
companyName: Joi.string(),
appName: Joi.string(),
bundleIdentifier: Joi.string(),
});
export const SubmitProfileSchema = Joi.object({
extends: Joi.string(),
android: AndroidSubmitProfileSchema,
ios: IosSubmitProfileSchema,
});
@@ -19,6 +19,7 @@ export interface AndroidSubmitProfile {
track: AndroidReleaseTrack;
releaseStatus: AndroidReleaseStatus;
changesNotSentForReview: boolean;
applicationId?: string;
}
export const AndroidSubmitProfileFieldsToEvaluate: (keyof AndroidSubmitProfile)[] = [
@@ -36,6 +37,7 @@ export interface IosSubmitProfile {
language: string;
companyName?: string;
appName?: string;
bundleIdentifier?: string;
}
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
@@ -45,10 +47,10 @@ export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
];
export type SubmitProfile<TPlatform extends Platform = Platform> =
TPlatform extends Platform.ANDROID
? AndroidSubmitProfile
: TPlatform extends Platform.IOS
? IosSubmitProfile
: TPlatform extends Platform
? AndroidSubmitProfile | IosSubmitProfile
: never;
TPlatform extends Platform.ANDROID ? AndroidSubmitProfile : IosSubmitProfile;
export interface EasJsonSubmitProfile {
extends?: string;
[Platform.ANDROID]?: AndroidSubmitProfile;
[Platform.IOS]?: IosSubmitProfile;
}
+21
View File
@@ -0,0 +1,21 @@
import { EasJsonBuildProfile } from './build/types';
import { EasJsonSubmitProfile } from './submit/types';
export type ProfileType = 'build' | 'submit';
export type EasJsonProfile<T extends ProfileType> = T extends 'build'
? EasJsonBuildProfile
: EasJsonSubmitProfile;
export enum CredentialsSource {
LOCAL = 'local',
REMOTE = 'remote',
}
export interface EasJson {
cli?: {
version?: string;
requireCommit?: boolean;
};
build?: { [profileName: string]: EasJsonBuildProfile };
submit?: { [profileName: string]: EasJsonSubmitProfile };
}
+1 -1
View File
@@ -28,6 +28,6 @@
"@types/node": "16.7.8",
"@types/semver": "7.3.8",
"ts-node": "10.2.1",
"typescript": "4.4.2"
"typescript": "4.5.2"
}
}
+4 -4
View File
@@ -176,10 +176,10 @@ [email protected]:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
typescript@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
typescript@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
universalify@^2.0.0:
version "2.0.0"
Generated
+106 -76
View File
@@ -848,14 +848,14 @@
mv "~2"
safe-json-stringify "~1"
"@expo/[email protected].3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.3.tgz#8680dea2709c70f3a9c9e77ca6057335758341a8"
integrity sha512-vJ+4YOXxAjznpIG0WQHNsbeeJQXS/ZxzVPHhOO3le8Lkl+WJ2eH7XypulPRalBI4E0WLFtzDzLIdnqYJyjzvaw==
"@expo/[email protected].9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.9.tgz#52c5bee8411185fc4a340072fbed3584d0409234"
integrity sha512-3wv0uhG5U5O4BGX2qm4gjFK3jvYVTy0raFsR6B/2zoBNgD3lXF8Ae5osVN118aKNZb2B7SMx3V7McunZhf7RXg==
dependencies:
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
"@expo/plist" "0.0.15"
"@expo/config-types" "^43.0.1"
"@expo/json-file" "8.2.34"
"@expo/plist" "0.0.16"
"@react-native/normalize-color" "^2.0.0"
chalk "^4.1.2"
debug "^4.3.1"
@@ -869,20 +869,20 @@
xcode "^3.0.1"
xml2js "0.4.23"
"@expo/config-types@^43.0.0":
version "43.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-43.0.0.tgz#e283a4a1b72a655a6a2a745bd277caef58c8edb0"
integrity sha512-GBbEtLdGX40DJ1wTixWtOHseupA5xWclV57KNZ58ebauWAOyK3Fyni7GD2OCOzxTIFi1nSjqgO9JlMp3ayP4Tw==
"@expo/config-types@^43.0.1":
version "43.0.1"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-43.0.1.tgz#3e047dccb371741a540980eaff26fb0c95039c30"
integrity sha512-EtllpCGDdB/UdwAIs5YXJwBLpbFQNdlLLrxIvoILA9cXrpQMWkeDCT9lQPJzFRMFcLUaMuGvkzX2tR4tx5EQFQ==
"@expo/[email protected].3":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.3.tgz#48e5d453ae00ca090ea47799f4a9f352d5136a89"
integrity sha512-Qo2OVDfKbCyLiG3g2vqrPQXBBGRB2I3ADIB/8nZ3Ypy7QhqOA1L7WYhTQQr3T1zknHTMzy7u61ArHweiAnEEpw==
"@expo/[email protected].9":
version "6.0.9"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.9.tgz#d2f700e067581f7fb088c4e811b38351ac57e151"
integrity sha512-UJs8b5SSU9dl4VrX+MiSJcSwUVt7u6oUfNQDJ6VIWu03lDkNKtrk64GeSmUqHKsFd6iFXaMxtvkTBVmWWM6NaA==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "4.0.3"
"@expo/config-types" "^43.0.0"
"@expo/json-file" "8.2.33"
"@expo/config-plugins" "4.0.9"
"@expo/config-types" "^43.0.1"
"@expo/json-file" "8.2.34"
getenv "^1.0.0"
glob "7.1.6"
require-from-string "^2.0.2"
@@ -899,10 +899,10 @@
joi "^17.4.2"
semver "^7.3.5"
"@expo/[email protected]7":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.17.tgz#75e2606749ffa1284de570245f668503e4a06c3f"
integrity sha512-zaOj24JK5F+pPGpULP8D9aAbApcR7ixV2GjT43YzZqjHuyxQ5knI+EsbmZ1TNnA1vNJJKMYrigB5+chcUGwkpw==
"@expo/[email protected]8":
version "0.3.18"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.18.tgz#36a41ab2925974ac5707e02cb0d3694349140497"
integrity sha512-77/ub2aGuf7SYfaFhvCHE54Hs/jRuU5j+pemS5seLfVHNwHbJSse91TMhsTLLNz3GwwqTxFVe3KMycSccJ73nA==
dependencies:
"@expo/spawn-async" "1.5.0"
chalk "^4.0.0"
@@ -925,6 +925,15 @@
json5 "^1.0.1"
write-file-atomic "^2.3.0"
"@expo/[email protected]":
version "8.2.34"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.34.tgz#2f24e90a677195f7a81e167115460eb2902c3130"
integrity sha512-ZxtBodAZGxdLtgKzmsC+8ViUxt1mhFW642Clu2OuG3f6PAyAFsU/SqEGag9wKFaD3x3Wt8VhL+3y5fMJmUFgPw==
dependencies:
"@babel/code-frame" "~7.10.4"
json5 "^1.0.1"
write-file-atomic "^2.3.0"
"@expo/[email protected]":
version "1.24.0-expo"
resolved "https://registry.yarnpkg.com/@expo/oclif-dev-cli/-/oclif-dev-cli-1.24.0-expo.tgz#4e1f766e79b4d99e3da29318a577952793b143bf"
@@ -945,26 +954,17 @@
qqjs "^0.3.10"
tslib "^1.9.3"
"@expo/[email protected].4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@expo/pkcs12/-/pkcs12-0.0.4.tgz#d4ba48bfcb5dcae979e96644a0ca157c0d70f075"
integrity sha512-dYNecU+r4RIy8dPzu1SCUi+EqPhfSic6ODH6IkKLCf9m7unxEO0SvhU+rJqlgpoQ49PThOmk4EFX2KCClyrKnw==
"@expo/[email protected].6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@expo/pkcs12/-/pkcs12-0.0.6.tgz#ef9e4691bfced9318d4a3736260ced27401fc752"
integrity sha512-glYvPd2fv9tAQbXWprhnUYmKGmOtFJzGxhBef90bkZL8N4yNhcF1T/+l1Nrtzrf/ZSAjCnek4oXezpwOUYBnWw==
dependencies:
node-forge "^0.10.0"
"@expo/[email protected]4":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.14.tgz#a756903bd28aabe0a961222df2e7858a39a218c9"
integrity sha512-bb4Ua1M/OdNgS8KiGdSDUjZ/bbPfv3xdPY/lz8Ctp/adlj/QgB8xA7tVPeqSSfJPZqFRwU0qLCnRhpUOnP51VQ==
dependencies:
"@xmldom/xmldom" "~0.7.0"
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/[email protected]":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.15.tgz#41ef37b7bbe6b81c48bf4a5c359661c766bb9e90"
integrity sha512-LDxiS0KNZAGJu4fIJhbEKczmb+zeftl1NU0LE0tj0mozoMI5HSKdMUchgvnBm35bwBl8ekKkAfJJ0ONxljWQjQ==
"@expo/[email protected]6":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.16.tgz#c7d294a774196cfa1d8caf89a75ba923f5d1d53c"
integrity sha512-yQMt2CTm2VNLiHv2/EqHYrHvfflI2mFJd+DZIQQy569hvpZle0CxMPGH1p2KatbrO8htxJNvwrlR/4TIwhQJ5w==
dependencies:
"@xmldom/xmldom" "~0.7.0"
base64-js "^1.2.3"
@@ -985,30 +985,31 @@
lodash.template "^4.4.0"
semver "^7.3.2"
"@expo/[email protected].3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.3.tgz#448f48aab2a28af6b12dc1a95696dbd08f5668f3"
integrity sha512-7r/Wfzs2QNm8RvnbT1LFZ+Xkb3vrcLVGwkRt1BHPxmRFwq4SNwhkMyLPopQEjUhJU3AqLn6EKItS39+9c2f8vg==
"@expo/[email protected].9":
version "3.0.9"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.9.tgz#c9a7034d4299bd7b3bf36c17efbdf1772899c7fe"
integrity sha512-j4cHyZe04WijAyH2sJS2pH8DltqPDVQczN6brqFiuRktyIAroqh4ZMY1fgpSweX18LTXmGF9d1fG1aXC+N64YQ==
dependencies:
"@expo/config" "6.0.3"
"@expo/config-plugins" "4.0.3"
"@expo/config-types" "^43.0.0"
"@expo/image-utils" "0.3.17"
"@expo/json-file" "8.2.33"
"@expo/config" "6.0.9"
"@expo/config-plugins" "4.0.9"
"@expo/config-types" "^43.0.1"
"@expo/image-utils" "0.3.18"
"@expo/json-file" "8.2.34"
debug "^4.3.1"
fs-extra "^9.0.0"
resolve-from "^5.0.0"
semver "7.3.2"
xml2js "0.4.23"
"@expo/[email protected]":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/results/-/results-1.0.0.tgz#fd4b22f936ceafce23b04799f54b87fe2a9e18d1"
integrity sha512-qECzzXX5oJot3m2Gu9pfRDz50USdBieQVwYAzeAtQRUTD3PVeTK1tlRUoDcrK8PSruDLuVYdKkLebX4w/o55VA==
"@expo/[email protected].0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.0.tgz#90c6ef615fbf34473180aa439e3daac7b5af9eed"
integrity sha512-XUxJi2xUEi3RNdKc78emywcl8jQ+iGzpBq0gC1Gpu2/3gAuIxIfp/vGWp1vWo58cZtabhbQx/AMmbOfsHK35Qw==
"@expo/[email protected].1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz#6aa575f346833eb6290282118766d4919c808c6a"
integrity sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==
dependencies:
"@expo/bunyan" "^4.0.0"
"@segment/loosely-validate-event" "^2.0.0"
@@ -1018,11 +1019,6 @@
remove-trailing-slash "^0.1.0"
uuid "^8.3.2"
"@expo/[email protected]":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c"
integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==
"@expo/[email protected]":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.5.0.tgz#799827edd8c10ef07eb1a2ff9dcfe081d596a395"
@@ -2383,7 +2379,19 @@
node-gyp "^7.1.0"
read-package-json-fast "^2.0.1"
"@oclif/[email protected].0", "@oclif/command@^1.5.13", "@oclif/command@^1.5.20", "@oclif/command@^1.6.0":
"@oclif/[email protected].3", "@oclif/command@^1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.3.tgz#1f8bc2c4ecf94b6659a5134d95da179e1dffad9e"
integrity sha512-OGjrhdVgTT2TAAj/2RrdXjwxaDoTm16c2LfAzrta1xIFe6/XhgQIYDmeRN/RptQoZQBX8e9Vv2JoQq+TbghJmw==
dependencies:
"@oclif/config" "^1.15.1"
"@oclif/errors" "^1.3.5"
"@oclif/parser" "^3.8.5"
"@oclif/plugin-help" "^3.2.4"
debug "^4.1.1"
semver "^7.3.2"
"@oclif/command@^1.5.13", "@oclif/command@^1.5.20", "@oclif/command@^1.6.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.0.tgz#c1a499b10d26e9d1a611190a81005589accbb339"
integrity sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw==
@@ -2395,7 +2403,19 @@
debug "^4.1.1"
semver "^7.3.2"
"@oclif/[email protected].0", "@oclif/config@^1.12.12", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1":
"@oclif/[email protected].1", "@oclif/config@^1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.1.tgz#383515f6715b91d8df5db8108214e93bb46e86ca"
integrity sha512-UqV5qsN2np96TNlJspSNlRl7CpFmxYSrB0iLe3XV9NDkbFEE5prGP++h6w6xOR/FL3QV7BoqrbwGuJdJdFbidw==
dependencies:
"@oclif/errors" "^1.3.3"
"@oclif/parser" "^3.8.6"
debug "^4.1.1"
globby "^11.0.1"
is-wsl "^2.1.1"
tslib "^2.0.0"
"@oclif/config@^1.12.12", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.0.tgz#ba8639118633102a7e481760c50054623d09fcab"
integrity sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA==
@@ -2407,7 +2427,7 @@
is-wsl "^2.1.1"
tslib "^2.0.0"
"@oclif/[email protected]":
"@oclif/[email protected]", "@oclif/errors@^1.3.5":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c"
integrity sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==
@@ -2444,17 +2464,27 @@
chalk "^2.4.2"
tslib "^1.9.3"
"@oclif/p[email protected]":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.3.tgz#cd24010e7eb326782843d3aa6d6b5a4affebb2c3"
integrity sha512-l2Pd0lbOMq4u/7xsl9hqISFqyR9gWEz/8+05xmrXFr67jXyS6EUCQB+mFBa0wepltrmJu0sAFg9AvA2mLaMMqQ==
"@oclif/parser@^3.8.5", "@oclif/parser@^3.8.6":
version "3.8.6"
resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.6.tgz#d5a108af9c708a051cc6b1d27d47359d75f41236"
integrity sha512-tXb0NKgSgNxmf6baN6naK+CCwOueaFk93FG9u202U7mTBHUKsioOUlw1SG/iPi9aJM3WE4pHLXmty59pci0OEw==
dependencies:
"@oclif/command" "^1.5.20"
"@oclif/config" "^1.15.1"
"@oclif/errors" "^1.2.2"
"@oclif/linewrap" "^1.0.0"
chalk "^4.1.0"
tslib "^2.0.0"
"@oclif/[email protected]", "@oclif/plugin-help@^3.2.4":
version "3.2.5"
resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.5.tgz#dab0f5e655971b2c49dd80ee472f97b6e69a1297"
integrity sha512-fjkZTstvacCPicF2oaa3Lc+Yw3ocKEaW6x6O7doVqMLuoMUX6wBOQ+f1a3VFzO1fErqNeFPDlUlVUhwq9yMzQg==
dependencies:
"@oclif/command" "^1.8.3"
"@oclif/config" "^1.17.1"
"@oclif/errors" "^1.3.5"
chalk "^4.1.0"
indent-string "^4.0.0"
lodash.template "^4.4.0"
lodash "^4.17.21"
string-width "^4.2.0"
strip-ansi "^6.0.0"
widest-line "^3.1.0"
@@ -7543,7 +7573,7 @@ lodash.without@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
[email protected], lodash@^4.7.0:
[email protected], lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -8882,10 +8912,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
prettier@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
pretty-format@^27.0.0, pretty-format@^27.1.0:
version "27.1.0"
@@ -10460,10 +10490,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
typescript@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
ua-parser-js@^0.7.18:
version "0.7.22"