Compare commits

..
Author SHA1 Message Date
Mustafa Abban bad188babe Merge branch 'main' into eas-build-valid-lock 2026-04-30 10:46:50 -05:00
Mustafa Abban f6ba878b29 Merge branch 'main' into eas-build-valid-lock 2026-03-13 12:59:07 -05:00
AbbanMustafa 07fbe5c777 fmt 2026-03-11 14:32:41 -05:00
AbbanMustafa c8739b66af changelog 2026-03-11 14:16:43 -05:00
AbbanMustafa 777a3c947f package lockfile-utils 2026-03-11 14:14:17 -05:00
AbbanMustafa ce3d17ae4c casing 2026-03-11 13:47:16 -05:00
AbbanMustafa 2f9dfb6610 env var to skip check 2026-03-11 13:45:53 -05:00
AbbanMustafa b862a0575c turtlebot 2026-03-02 16:02:30 -06:00
AbbanMustafa c1fe243a70 peer dependency conflicts 2026-03-02 15:33:39 -06:00
AbbanMustafa 8065715d4c changelog 2026-03-02 16:14:29 -05:00
AbbanMustafa 402cc40bb9 clean log 2026-03-02 15:57:11 -05:00
AbbanMustafa 61eeabb404 verify package versions 2026-03-02 15:48:01 -05:00
AbbanMustafa edbaaf8000 warn on staleness 2026-03-01 16:34:18 -05:00
AbbanMustafa 68e561e879 [eas-build] - lockfile preflight checks to eas build command 2026-02-26 12:51:07 -05:00
51 changed files with 1386 additions and 3667 deletions

No files matched your search

-4
View File
@@ -8,13 +8,9 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- [eas-cli] Add `eas integrations:convex` commands to manage Convex integrations for EAS projects. ([#3575](https://github.com/expo/eas-cli/pull/3575) by [@fiberjw](https://github.com/fiberjw))
### 🐛 Bug fixes
- [eas-cli] Create portable project archives on all platforms to normalize cross-platform tar metadata and permissions. ([#3234](https://github.com/expo/eas-cli/pull/3234) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Remove hardcoded `builderEnvironment.image` override in `eas build:resign`. ([#3661](https://github.com/expo/eas-cli/pull/3661) by [@hSATAC](https://github.com/hSATAC))
- [eas-cli] Fix `eas update --json` intermittently failing with JSON parse errors during "Computing project fingerprints" by passing `silent: true` to `@expo/fingerprint` to suppress subprocess stdout pollution. ([#3659](https://github.com/expo/eas-cli/pull/3659) by [@Mookiies](https://github.com/Mookiies))
### 🧹 Chores
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "18.10.0",
"version": "18.9.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/build-tools",
"version": "18.10.0",
"version": "18.9.0",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
"author": "Expo <[email protected]>",
+1 -1
View File
@@ -50,7 +50,7 @@ yarn generate-graphql-code # Generate TypeScript types from GraphQL schema
yarn verify-graphql-code # Verify GraphQL code is up to date (CI check)
```
The GraphQL schema is fetched from the API URL resolved by `src/api.ts` and generates:
The GraphQL schema is fetched from `https://staging-api.expo.dev/graphql` and generates:
- `src/graphql/generated.ts` - 499KB+ of TypeScript types
- Types for all queries, mutations, and fragments
+93 -223
View File
@@ -120,13 +120,6 @@ eas --help COMMAND
* [`eas integrations:asc:connect`](#eas-integrationsascconnect)
* [`eas integrations:asc:disconnect`](#eas-integrationsascdisconnect)
* [`eas integrations:asc:status`](#eas-integrationsascstatus)
* [`eas integrations:convex:connect`](#eas-integrationsconvexconnect)
* [`eas integrations:convex:dashboard`](#eas-integrationsconvexdashboard)
* [`eas integrations:convex:project`](#eas-integrationsconvexproject)
* [`eas integrations:convex:project:delete`](#eas-integrationsconvexprojectdelete)
* [`eas integrations:convex:team`](#eas-integrationsconvexteam)
* [`eas integrations:convex:team:delete [CONVEX_TEAM]`](#eas-integrationsconvexteamdelete-convex_team)
* [`eas integrations:convex:team:invite [CONVEX_TEAM]`](#eas-integrationsconvexteaminvite-convex_team)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:lint`](#eas-metadatalint)
@@ -190,7 +183,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -207,7 +200,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:usage [ACCOUNT_NAME]`
@@ -229,7 +222,7 @@ DESCRIPTION
view account usage and billing for the current cycle
```
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/account/usage.ts)_
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/account/usage.ts)_
## `eas account:view`
@@ -246,7 +239,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -260,7 +253,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -312,7 +305,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -333,7 +326,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -353,7 +346,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -373,7 +366,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -396,7 +389,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -434,7 +427,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -454,7 +447,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -472,7 +465,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -492,7 +485,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -514,7 +507,7 @@ DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/dev.ts)_
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -541,7 +534,7 @@ DESCRIPTION
download a simulator/emulator build by build ID or fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -579,7 +572,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -619,7 +612,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -649,7 +642,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -675,7 +668,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -727,7 +720,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -745,7 +738,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -763,7 +756,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -780,7 +773,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -801,7 +794,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:delete [NAME]`
@@ -822,7 +815,7 @@ DESCRIPTION
Delete a channel
```
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/delete.ts)_
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/delete.ts)_
## `eas channel:edit [NAME]`
@@ -844,7 +837,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:insights`
@@ -868,7 +861,7 @@ DESCRIPTION
display adoption, crash, and unique-user insights for a channel + runtime version
```
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/insights.ts)_
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/insights.ts)_
## `eas channel:list`
@@ -888,7 +881,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -910,7 +903,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -932,7 +925,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -967,7 +960,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -990,7 +983,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -1011,7 +1004,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -1028,7 +1021,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -1046,7 +1039,7 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -1075,7 +1068,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/deploy/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -1100,7 +1093,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
## `eas deploy:alias:delete [ALIAS_NAME]`
@@ -1121,7 +1114,7 @@ ALIASES
$ eas worker:alias:delete
```
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
## `eas deploy:delete [DEPLOYMENT_ID]`
@@ -1142,7 +1135,7 @@ ALIASES
$ eas worker:delete
```
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/deploy/delete.ts)_
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/deploy/delete.ts)_
## `eas deploy:promote`
@@ -1179,7 +1172,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1199,7 +1192,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1220,7 +1213,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1241,7 +1234,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1255,7 +1248,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1269,7 +1262,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1301,7 +1294,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1327,7 +1320,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1349,7 +1342,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1377,7 +1370,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1405,7 +1398,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1428,7 +1421,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1450,7 +1443,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1484,7 +1477,7 @@ DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1528,7 +1521,7 @@ EXAMPLES
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1559,7 +1552,7 @@ EXAMPLES
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1638,7 +1631,7 @@ DESCRIPTION
connect a project to an App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
## `eas integrations:asc:disconnect`
@@ -1657,7 +1650,7 @@ DESCRIPTION
disconnect the current project from its App Store Connect app
```
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
## `eas integrations:asc:status`
@@ -1675,130 +1668,7 @@ DESCRIPTION
show the App Store Connect app link status for the current project
```
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
## `eas integrations:convex:connect`
connect Convex to your Expo project
```
USAGE
$ eas integrations:convex:connect [--non-interactive] [--region aws-us-east-1|aws-eu-west-1] [--team-name <value>]
[--project-name <value>]
FLAGS
--non-interactive Run the command in non-interactive mode.
--project-name=<value> Name for the Convex project (defaults to app slug)
--region=<option> Convex deployment region (e.g. aws-us-east-1, aws-eu-west-1)
<options: aws-us-east-1|aws-eu-west-1>
--team-name=<value> Name for the new Convex team (defaults to EAS account name)
DESCRIPTION
connect Convex to your Expo project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
## `eas integrations:convex:dashboard`
open the Convex dashboard for the linked Convex project
```
USAGE
$ eas integrations:convex:dashboard
DESCRIPTION
open the Convex dashboard for the linked Convex project
```
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
## `eas integrations:convex:project`
display the Convex project linked to the current Expo app
```
USAGE
$ eas integrations:convex:project
DESCRIPTION
display the Convex project linked to the current Expo app
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/project.ts)_
## `eas integrations:convex:project:delete`
remove the Convex project link for the current Expo app from EAS servers
```
USAGE
$ eas integrations:convex:project:delete [--non-interactive] [-y]
FLAGS
-y, --yes Skip confirmation prompt
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
remove the Convex project link for the current Expo app from EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
## `eas integrations:convex:team`
display Convex teams linked to the current Expo app's owner account
```
USAGE
$ eas integrations:convex:team
DESCRIPTION
display Convex teams linked to the current Expo app's owner account
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/team.ts)_
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
remove a Convex team link from the current Expo app owner account's EAS servers
```
USAGE
$ eas integrations:convex:team:delete [CONVEX_TEAM] [--non-interactive] [-y]
ARGUMENTS
[CONVEX_TEAM] Slug of the Convex team to remove
FLAGS
-y, --yes Skip confirmation prompt
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
remove a Convex team link from the current Expo app owner account's EAS servers
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
send a Convex team invitation to your verified email address
```
USAGE
$ eas integrations:convex:team:invite [CONVEX_TEAM] [--non-interactive]
ARGUMENTS
[CONVEX_TEAM] Slug of the Convex team to invite yourself to
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
send a Convex team invitation to your verified email address
```
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
## `eas login`
@@ -1850,7 +1720,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1868,7 +1738,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1886,7 +1756,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas new [PATH]`
@@ -1938,7 +1808,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1952,7 +1822,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1975,7 +1845,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:new [PATH]`
@@ -1999,7 +1869,7 @@ ALIASES
$ eas new
```
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/project/new.ts)_
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/project/new.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -2017,7 +1887,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -2050,7 +1920,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -2093,7 +1963,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -2114,7 +1984,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -2135,7 +2005,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -2160,7 +2030,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:insights GROUPID`
@@ -2187,7 +2057,7 @@ DESCRIPTION
display launch, crash, unique-user, and size insights for an update group
```
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/insights.ts)_
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/insights.ts)_
## `eas update:list`
@@ -2214,7 +2084,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -2252,7 +2122,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
@@ -2280,7 +2150,7 @@ DESCRIPTION
revert a rollout update for a project
```
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -2310,7 +2180,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -2331,7 +2201,7 @@ DESCRIPTION
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -2355,7 +2225,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
@@ -2376,7 +2246,7 @@ DESCRIPTION
upload a local build and generate a sharable link
```
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/upload.ts)_
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -2398,7 +2268,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -2418,7 +2288,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -2437,7 +2307,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -2460,7 +2330,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -2477,7 +2347,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2598,7 +2468,7 @@ DESCRIPTION
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
@@ -2618,7 +2488,7 @@ DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:logs [ID]`
@@ -2642,7 +2512,7 @@ DESCRIPTION
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/logs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/logs.ts)_
## `eas workflow:run FILE`
@@ -2682,7 +2552,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
@@ -2704,7 +2574,7 @@ DESCRIPTION
list recent workflow runs for this project, with their IDs, statuses, and timestamps
```
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/runs.ts)_
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:status [WORKFLOW_RUN_ID]`
@@ -2732,7 +2602,7 @@ FLAG DESCRIPTIONS
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/status.ts)_
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/status.ts)_
## `eas workflow:validate PATH`
@@ -2752,7 +2622,7 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/validate.ts)_
## `eas workflow:view [ID]`
@@ -2774,5 +2644,5 @@ DESCRIPTION
workflow runs for the current project.
```
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.10.0/packages/eas-cli/src/commands/workflow/view.ts)_
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.9.1/packages/eas-cli/src/commands/workflow/view.ts)_
<!-- commandsstop -->
-28
View File
@@ -1,28 +0,0 @@
import type { CodegenConfig } from '@graphql-codegen/cli';
import { getExpoApiBaseUrl } from './src/api';
const schema =
process.env.GRAPHQL_SCHEMA_URL ?? new URL('/graphql', getExpoApiBaseUrl()).toString();
const config: CodegenConfig = {
overwrite: true,
schema,
documents: ['src/**/!(*.d).ts'],
generates: {
'src/graphql/generated.ts': {
plugins: ['typescript', 'typescript-operations'],
config: {
dedupeOperationSuffix: true,
},
hooks: {
afterOneFileWrite: ['node ./scripts/annotate-graphql-codegen.js'],
},
},
'./graphql.schema.json': {
plugins: ['introspection'],
},
},
};
export default config;
+17
View File
@@ -0,0 +1,17 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/**/!(*.d).ts'
generates:
src/graphql/generated.ts:
plugins:
- 'typescript'
- 'typescript-operations'
config:
dedupeOperationSuffix: true
hooks:
afterOneFileWrite:
- 'node ./scripts/annotate-graphql-codegen.js'
./graphql.schema.json:
plugins:
- 'introspection'
+1 -324
View File
@@ -25488,18 +25488,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resolvedImage",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resourceClass",
"description": "The builder resource class requested by the developer",
@@ -25615,7 +25603,7 @@
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use 'resolvedImage' for the concrete image the build runs on."
"deprecationReason": "Check logs instead."
},
{
"name": "status",
@@ -29837,83 +29825,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ConvexProjectMutation",
"description": null,
"fields": [
{
"name": "deleteConvexProject",
"description": null,
"args": [
{
"name": "convexProjectId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setupConvexProject",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "SetupConvexProjectInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SetupConvexProjectResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ConvexTeamConnection",
@@ -29951,38 +29862,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexTeamName",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexTeamSlug",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": null,
@@ -30015,30 +29894,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "invitedAt",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "invitedEmail",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": null,
@@ -55847,22 +55702,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexProject",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ConvexProjectMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexTeamConnection",
"description": null,
@@ -60162,152 +60001,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "SetupConvexProjectInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexTeamConnectionId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deploymentRegion",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectName",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SetupConvexProjectResult",
"description": null,
"fields": [
{
"name": "convexDeploymentName",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexDeploymentUrl",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "convexProject",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ConvexProject",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deployKey",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SizeBreakdownCategory",
@@ -62368,22 +62061,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "includedAgentCreditsInCents",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isDowngrading",
"description": null,
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli",
"version": "18.10.0",
"version": "18.9.1",
"description": "EAS command line tool",
"keywords": [
"cli",
@@ -37,7 +37,7 @@
"typecheck-for-build": "tsc --project tsconfig.build.json",
"test": "jest",
"version": "yarn run -T oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.ts",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"verify-graphql-code": "./scripts/verify-graphql.sh",
"clean": "rimraf dist build tmp node_modules yarn-error.log \"*.tsbuildinfo\"",
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
@@ -202,7 +202,7 @@
"description": "manage project and account environment variables"
},
"integrations": {
"description": "manage third-party service integrations"
"description": "manage service connections"
},
"metadata": {
"description": "manage store configuration"
-346
View File
@@ -1,346 +0,0 @@
import { spawn } from 'child_process';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
/*
* Manual Convex integration E2E runner.
*
* Run from packages/eas-cli with:
* yarn ts-node scripts/convex-e2e.ts
*
* This uses the deployed EAS API and real authenticated EAS/Convex-backed
* resources. Cleanup removes EAS-side links when possible, but it does not
* destroy Convex-side resources.
*
* Options:
* CONVEX_E2E_KEEP_PROJECT=1 keeps the temp Expo project and skips cleanup.
* CONVEX_E2E_SKIP_DASHBOARD=1 skips opening the Convex dashboard.
* CONVEX_E2E_ACCOUNT=<account-name> sets the Expo app owner before eas init.
*/
type RunOptions = {
cwd?: string;
allowFailure?: boolean;
env?: NodeJS.ProcessEnv;
};
type RunResult = {
stdout: string;
stderr: string;
exitCode: number | null;
};
const KEEP_PROJECT = process.env.CONVEX_E2E_KEEP_PROJECT === '1';
const SKIP_DASHBOARD = process.env.CONVEX_E2E_SKIP_DASHBOARD === '1';
const ACCOUNT = process.env.CONVEX_E2E_ACCOUNT;
const SCRIPT_DIR = __dirname;
const EAS_CLI_ROOT = path.resolve(SCRIPT_DIR, '..');
const LOCAL_EAS_BIN = path.join(EAS_CLI_ROOT, 'bin', 'run');
const TEMP_ROOT_PREFIX = path.join(os.tmpdir(), 'eas-cli-convex-e2e-');
let tempRoot: string | null = null;
let projectDir: string | null = null;
let initializedProject = false;
let initialConvexTeamSlugs: string[] = [];
let createdConvexTeamSlug: string | null = null;
async function mainAsync(): Promise<void> {
try {
await assertFileExistsAsync(
LOCAL_EAS_BIN,
`Local EAS CLI binary not found at ${LOCAL_EAS_BIN}`
);
await runEasAsync(['whoami']);
const testName = `convex-e2e-${Date.now().toString(36)}`;
tempRoot = await fs.mkdtemp(TEMP_ROOT_PREFIX);
projectDir = path.join(tempRoot, testName);
logStep(`Creating fresh Expo project ${testName}`);
await runAsync('npx', ['--yes', 'create-expo-app@latest', projectDir, '--template', 'blank'], {
cwd: tempRoot,
env: getNpxEnvironment(),
});
await updateAppJsonAsync(projectDir, testName);
logStep('Creating/linking EAS project');
await runEasAsync(['init', '--non-interactive', '--force'], projectDir);
initializedProject = true;
initialConvexTeamSlugs = parseConvexTeamSlugs(
(await runEasAsync(['integrations:convex:team'], projectDir)).stdout
);
logStep('Connecting Convex');
await runEasAsync(
['integrations:convex:connect', '--non-interactive', '--project-name', testName],
projectDir
);
await assertEnvLocalAsync(projectDir);
await assertPackageJsonDependencyAsync(projectDir, 'convex');
const teamOutput = await runEasAsync(['integrations:convex:team'], projectDir);
const projectOutput = await runEasAsync(['integrations:convex:project'], projectDir);
assertIncludes(
projectOutput.stdout,
'Dashboard',
'Expected project output to include dashboard'
);
const teamSlugsAfterConnect = parseConvexTeamSlugs(teamOutput.stdout);
const newTeamSlugs = teamSlugsAfterConnect.filter(
slug => !initialConvexTeamSlugs.includes(slug)
);
createdConvexTeamSlug = newTeamSlugs[0] ?? null;
const convexTeamSlug = createdConvexTeamSlug ?? teamSlugsAfterConnect[0];
if (!convexTeamSlug) {
throw new Error('Could not determine a Convex team slug from team output.');
}
await runEasAsync(
['integrations:convex:team:invite', convexTeamSlug, '--non-interactive'],
projectDir
);
if (SKIP_DASHBOARD) {
logStep('Skipping dashboard command because CONVEX_E2E_SKIP_DASHBOARD=1');
} else {
await runEasAsync(['integrations:convex:dashboard'], projectDir);
}
logStep('Convex E2E flow completed successfully');
} finally {
await cleanupAsync();
}
}
async function cleanupAsync(): Promise<void> {
if (!projectDir || !tempRoot) {
return;
}
if (KEEP_PROJECT) {
logStep('Skipping cleanup because CONVEX_E2E_KEEP_PROJECT=1');
log(`Temp project kept at: ${projectDir}`);
log('EAS-side links and Convex-side resources were left intact.');
return;
}
if (initializedProject) {
logStep('Cleaning up EAS-side Convex links');
const convexTeamSlugToDelete =
createdConvexTeamSlug ?? (await getCreatedConvexTeamSlugForCleanupAsync());
await runCleanupCommandAsync([
'integrations:convex:project:delete',
'--non-interactive',
'--yes',
]);
if (convexTeamSlugToDelete) {
await runCleanupCommandAsync([
'integrations:convex:team:delete',
convexTeamSlugToDelete,
'--non-interactive',
'--yes',
]);
} else {
warn(
'No newly-created Convex team link was detected. Skipping team link deletion to avoid removing a pre-existing EAS-side link.'
);
}
warn('Convex-side resources are not destroyed by this cleanup.');
}
await fs.rm(tempRoot, { recursive: true, force: true });
log(`Removed temp project directory: ${tempRoot}`);
}
async function getCreatedConvexTeamSlugForCleanupAsync(): Promise<string | null> {
if (!projectDir) {
return null;
}
const result = await runEasAsync(['integrations:convex:team'], projectDir, {
allowFailure: true,
});
if (result.exitCode !== 0) {
warn('Could not inspect Convex team links during cleanup.');
return null;
}
return (
parseConvexTeamSlugs(result.stdout).find(slug => !initialConvexTeamSlugs.includes(slug)) ?? null
);
}
async function runCleanupCommandAsync(args: string[]): Promise<void> {
try {
const result = await runEasAsync(args, projectDir ?? undefined, { allowFailure: true });
if (result.exitCode !== 0) {
warn(`Cleanup command exited with ${result.exitCode}: ${args.join(' ')}`);
}
} catch (error) {
warn(`Cleanup command failed: ${args.join(' ')}`);
warn(error instanceof Error ? error.message : String(error));
}
}
async function runEasAsync(
args: string[],
cwd?: string,
options: RunOptions = {}
): Promise<RunResult> {
return await runAsync(LOCAL_EAS_BIN, args, { cwd, ...options });
}
async function runAsync(
command: string,
args: string[],
{ cwd, allowFailure = false, env = {} }: RunOptions = {}
): Promise<RunResult> {
const printableCommand = [command, ...args].join(' ');
log(`\n$ ${printableCommand}`);
if (cwd) {
log(`cwd: ${cwd}`);
}
return await new Promise<RunResult>((resolve, reject) => {
const child = spawn(command, args, {
cwd,
env: {
...process.env,
...env,
CI: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
const chunk = data.toString();
stdout += chunk;
process.stdout.write(redact(chunk));
});
child.stderr.on('data', data => {
const chunk = data.toString();
stderr += chunk;
process.stderr.write(redact(chunk));
});
child.on('error', reject);
child.on('close', exitCode => {
const result = { stdout, stderr, exitCode };
if (!allowFailure && exitCode !== 0) {
reject(new Error(`Command failed with exit code ${exitCode}: ${printableCommand}`));
return;
}
resolve(result);
});
});
}
async function updateAppJsonAsync(appDir: string, testName: string): Promise<void> {
const appJsonPath = path.join(appDir, 'app.json');
const rawAppJson = await fs.readFile(appJsonPath, 'utf8');
const appJson = JSON.parse(rawAppJson);
appJson.expo = {
...appJson.expo,
name: testName,
slug: testName,
...(ACCOUNT ? { owner: ACCOUNT } : {}),
};
await fs.writeFile(appJsonPath, `${JSON.stringify(appJson, null, 2)}\n`);
}
async function assertEnvLocalAsync(appDir: string): Promise<void> {
const envLocalPath = path.join(appDir, '.env.local');
const envLocal = await fs.readFile(envLocalPath, 'utf8');
assertIncludes(
envLocal,
'CONVEX_DEPLOY_KEY=',
'Expected .env.local to contain CONVEX_DEPLOY_KEY'
);
assertIncludes(
envLocal,
'EXPO_PUBLIC_CONVEX_URL=',
'Expected .env.local to contain EXPO_PUBLIC_CONVEX_URL'
);
log('\nVerified .env.local contains Convex environment variables:');
log(redact(envLocal));
}
async function assertPackageJsonDependencyAsync(
appDir: string,
packageName: string
): Promise<void> {
const packageJsonPath = path.join(appDir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (!dependencies[packageName]) {
throw new Error(`Expected package.json to include ${packageName}`);
}
log(`Verified package.json includes ${packageName}@${dependencies[packageName]}`);
}
function parseConvexTeamSlugs(output: string): string[] {
const strippedOutput = stripAnsi(output);
return Array.from(strippedOutput.matchAll(/^Team:\s+.+?\s\/\s(.+)$/gm), match => match[1].trim());
}
function assertIncludes(value: string, expected: string, message: string): void {
if (!value.includes(expected)) {
throw new Error(message);
}
}
async function assertFileExistsAsync(filePath: string, message: string): Promise<void> {
try {
await fs.access(filePath);
} catch {
throw new Error(message);
}
}
function logStep(message: string): void {
log(`\n==> ${message}`);
}
function log(message: string): void {
process.stdout.write(`${message}\n`);
}
function warn(message: string): void {
process.stderr.write(`${message}\n`);
}
function redact(value: string): string {
return value
.replace(/^CONVEX_DEPLOY_KEY=.*$/gm, 'CONVEX_DEPLOY_KEY=<redacted>')
.replace(/dev:[^\s|]+\|[^\s]+/g, 'dev:<redacted>');
}
function stripAnsi(value: string): string {
return value.replace(/\u001b\[[0-9;]*m/g, '');
}
function getNpxEnvironment(): NodeJS.ProcessEnv {
return {
npm_config_user_agent: `npm/10 node/${process.versions.node} ${process.platform} ${process.arch}`,
npm_execpath: undefined,
};
}
mainAsync().catch(error => {
warn(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
@@ -0,0 +1,377 @@
import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { CommonContext } from '../context';
import { checkLockfileAsync } from '../validateLockfile';
import { Client } from '../../vcs/vcs';
jest.mock('fs');
jest.mock('@expo/package-manager', () => ({
NPM_LOCK_FILE: 'package-lock.json',
YARN_LOCK_FILE: 'yarn.lock',
PNPM_LOCK_FILE: 'pnpm-lock.yaml',
BUN_LOCK_FILE: 'bun.lockb',
BUN_TEXT_LOCK_FILE: 'bun.lock',
resolveWorkspaceRoot: jest.fn(() => null),
}));
const { resolveWorkspaceRoot } = jest.requireMock<{
resolveWorkspaceRoot: jest.Mock;
}>('@expo/package-manager');
const mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
function createMockContext(
overrides: Partial<{
projectDir: string;
requiredPackageManager: string | null;
isFileIgnoredAsync: (filePath: string) => Promise<boolean>;
getRootPathAsync: () => Promise<string>;
}> = {}
): CommonContext<Platform> {
const vcsClient = {
isFileIgnoredAsync: overrides.isFileIgnoredAsync ?? (async () => false),
getRootPathAsync: overrides.getRootPathAsync ?? (async () => '/project'),
} as unknown as Client;
return {
projectDir: overrides.projectDir ?? '/project',
requiredPackageManager: overrides.requiredPackageManager ?? null,
vcsClient,
} as unknown as CommonContext<Platform>;
}
const PACKAGE_JSON_WITH_REACT = JSON.stringify({
dependencies: { react: '19.0.4', 'react-native': '~0.79.0' },
});
const NPM_LOCKFILE_SYNCED = JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.4' },
'node_modules/react-native': { version: '0.79.4' },
},
});
const NPM_LOCKFILE_STALE = JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.0' },
'node_modules/react-native': { version: '0.79.4' },
},
});
// bun.lock uses JSONC with trailing commas
const BUN_LOCKFILE_STALE = `{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"react": "19.0.4",
},
},
},
"packages": {
"react": ["[email protected]", "", {}, "sha512-abc"],
},
}`;
const BUN_LOCKFILE_SYNCED = `{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"react": "19.0.4",
},
},
},
"packages": {
"react": ["[email protected]", "", {}, "sha512-abc"],
},
}`;
const YARN_LOCKFILE_STALE = `# yarn lockfile v1
[email protected]:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz"
`;
const YARN_LOCKFILE_SYNCED = `# yarn lockfile v1
[email protected]:
version "19.0.4"
resolved "https://registry.yarnpkg.com/react/-/react-19.0.4.tgz"
react-native@~0.79.0:
version "0.79.4"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.4.tgz"
`;
beforeEach(() => {
vol.reset();
resolveWorkspaceRoot.mockReturnValue(null);
mockProcessExit.mockClear();
delete process.env.EAS_BUILD_SKIP_LOCKFILE_CHECK;
});
describe(checkLockfileAsync, () => {
it('skips all checks when EAS_BUILD_SKIP_LOCKFILE_CHECK is set', async () => {
process.env.EAS_BUILD_SKIP_LOCKFILE_CHECK = '1';
// This would normally fail — no lockfile, no package manager
vol.fromJSON({ '/project/package.json': '{}' });
const ctx = createMockContext({ requiredPackageManager: null });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('exits when no package manager is detected', async () => {
vol.fromJSON({ '/project/package.json': '{}' });
const ctx = createMockContext({ requiredPackageManager: null });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('exits when npm is detected but package-lock.json is missing', async () => {
vol.fromJSON({ '/project/package.json': '{}' });
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('does not error when lockfile exists and is in sync', async () => {
vol.fromJSON({
'/project/package.json': PACKAGE_JSON_WITH_REACT,
'/project/package-lock.json': NPM_LOCKFILE_SYNCED,
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('exits when lockfile exists but is gitignored', async () => {
vol.fromJSON({
'/project/package.json': '{}',
'/project/yarn.lock': '# yarn lockfile',
});
const ctx = createMockContext({
requiredPackageManager: 'yarn',
isFileIgnoredAsync: async () => true,
});
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('exits when conflicting lockfiles from multiple managers exist', async () => {
vol.fromJSON({
'/project/package.json': '{}',
'/project/package-lock.json': NPM_LOCKFILE_SYNCED,
'/project/yarn.lock': '# yarn lockfile',
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('exits when npm lockfile version does not satisfy package.json specifier', async () => {
vol.fromJSON({
'/project/package.json': PACKAGE_JSON_WITH_REACT,
'/project/package-lock.json': NPM_LOCKFILE_STALE,
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('exits when bun.lock version does not satisfy package.json specifier', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({ dependencies: { react: '19.0.4' } }),
'/project/bun.lock': BUN_LOCKFILE_STALE,
});
const ctx = createMockContext({ requiredPackageManager: 'bun' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('does not error when bun.lock is in sync', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({ dependencies: { react: '19.0.4' } }),
'/project/bun.lock': BUN_LOCKFILE_SYNCED,
});
const ctx = createMockContext({ requiredPackageManager: 'bun' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('exits when yarn lockfile version does not satisfy package.json specifier', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({ dependencies: { react: '19.0.4' } }),
'/project/yarn.lock': YARN_LOCKFILE_STALE,
});
const ctx = createMockContext({ requiredPackageManager: 'yarn' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('does not error when yarn lockfile is in sync', async () => {
vol.fromJSON({
'/project/package.json': PACKAGE_JSON_WITH_REACT,
'/project/yarn.lock': YARN_LOCKFILE_SYNCED,
});
const ctx = createMockContext({ requiredPackageManager: 'yarn' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('finds lockfile at workspace root for monorepos', async () => {
vol.fromJSON({
'/monorepo/yarn.lock': YARN_LOCKFILE_SYNCED,
'/monorepo/packages/app/package.json': PACKAGE_JSON_WITH_REACT,
});
resolveWorkspaceRoot.mockReturnValue('/monorepo');
const ctx = createMockContext({
projectDir: '/monorepo/packages/app',
requiredPackageManager: 'yarn',
getRootPathAsync: async () => '/monorepo',
});
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('does not report a conflict when both bun.lock and bun.lockb are present', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({ dependencies: { react: '19.0.4' } }),
'/project/bun.lockb': 'binary',
'/project/bun.lock': BUN_LOCKFILE_SYNCED,
});
const ctx = createMockContext({ requiredPackageManager: 'bun' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('skips sync check for non-registry specifiers', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: {
'my-local-pkg': 'file:../local',
'my-workspace-pkg': 'workspace:*',
react: '19.0.4',
},
}),
'/project/package-lock.json': JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.4' },
},
}),
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
describe('peer dependency conflicts', () => {
it('exits when a direct dependency has an unsatisfied peer (npm)', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: { react: '19.0.0', 'react-dom': '19.2.4' },
}),
'/project/package-lock.json': JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.0' },
'node_modules/react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
},
}),
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('exits when a direct dependency has an unsatisfied peer (bun)', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: { react: '19.0.0', 'react-dom': '19.2.4' },
}),
'/project/bun.lock': `{
"lockfileVersion": 1,
"workspaces": { "": { "dependencies": { "react": "19.0.0", "react-dom": "19.2.4" } } },
"packages": {
"react": ["[email protected]", "", {}, "sha512-abc"],
"react-dom": ["[email protected]", "", { "peerDependencies": { "react": "^19.2.4" } }, "sha512-def"],
},
}`,
});
const ctx = createMockContext({ requiredPackageManager: 'bun' });
await expect(checkLockfileAsync(ctx)).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('does not error when peer dependencies are satisfied', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: { react: '19.2.4', 'react-dom': '19.2.4' },
}),
'/project/package-lock.json': JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.2.4' },
'node_modules/react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
},
}),
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('skips optional peer dependencies (npm peerDependenciesMeta)', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: { '@0no-co/graphql.web': '1.2.0' },
}),
'/project/package-lock.json': JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/@0no-co/graphql.web': {
version: '1.2.0',
peerDependencies: { graphql: '^14.0.0 || ^15.0.0 || ^16.0.0' },
peerDependenciesMeta: { graphql: { optional: true } },
},
},
}),
});
const ctx = createMockContext({ requiredPackageManager: 'npm' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
it('skips optional peer dependencies (bun optionalPeers)', async () => {
vol.fromJSON({
'/project/package.json': JSON.stringify({
dependencies: { '@0no-co/graphql.web': '1.2.0' },
}),
'/project/bun.lock': `{
"lockfileVersion": 1,
"workspaces": { "": { "dependencies": { "@0no-co/graphql.web": "1.2.0" } } },
"packages": {
"@0no-co/graphql.web": ["@0no-co/[email protected]", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-abc"],
},
}`,
});
const ctx = createMockContext({ requiredPackageManager: 'bun' });
await expect(checkLockfileAsync(ctx)).resolves.not.toThrow();
expect(mockProcessExit).not.toHaveBeenCalled();
});
});
});
@@ -23,6 +23,7 @@ import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { checkLockfileAsync } from './validateLockfile';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
import { Analytics } from '../analytics/AnalyticsManager';
@@ -84,6 +85,7 @@ import { Client } from '../vcs/vcs';
let metroConfigValidated = false;
let sdkVersionChecked = false;
let lockfileChecked = false;
let hasWarnedAboutUsageOverages = false;
export interface BuildFlags {
@@ -414,6 +416,11 @@ async function prepareAndStartBuildAsync({
env,
});
if (!lockfileChecked) {
await checkLockfileAsync(buildCtx);
lockfileChecked = true;
}
if (!hasWarnedAboutUsageOverages && !flags.localBuildOptions.localBuildMode) {
hasWarnedAboutUsageOverages = true;
Log.newLine();
@@ -1,75 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import * as tar from 'tar';
import { Client } from '../../../vcs/vcs';
import { makeProjectTarballAsync } from '../repository';
jest.mock('../../../ora', () => ({
ora: jest.fn(() => ({
fail: jest.fn(),
isSpinning: false,
start: jest.fn(),
succeed: jest.fn(),
})),
}));
class FakeVcsClient extends Client {
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
await fs.mkdirp(path.join(destinationPath, 'bin'));
await fs.mkdirp(path.join(destinationPath, 'read-only-dir'));
await fs.writeFile(path.join(destinationPath, 'bin', 'postcheckout.sh'), '#!/bin/sh\necho hi\n');
await fs.writeFile(path.join(destinationPath, 'regular-file.txt'), 'regular file\n');
await fs.writeFile(path.join(destinationPath, 'read-only-dir', 'child.txt'), 'child file\n');
await fs.chmod(path.join(destinationPath, 'bin', 'postcheckout.sh'), 0o755);
await fs.chmod(path.join(destinationPath, 'regular-file.txt'), 0o644);
await fs.chmod(path.join(destinationPath, 'read-only-dir'), 0o555);
}
public async getRootPathAsync(): Promise<string> {
return process.cwd();
}
public canGetLastCommitMessage(): boolean {
return false;
}
}
describe(makeProjectTarballAsync, () => {
it('creates portable project archives while preserving executable files', async () => {
const removeAsync = fs.remove.bind(fs);
const removeSpy = jest.spyOn(fs, 'remove').mockImplementation(async targetPath => {
await fs.chmod(path.join(targetPath.toString(), 'read-only-dir'), 0o755).catch(() => {});
await removeAsync(targetPath);
});
const projectTarball = await makeProjectTarballAsync(new FakeVcsClient());
const entries = new Map<string, tar.ReadEntry>();
try {
await tar.list({
file: projectTarball.path,
onentry: entry => {
entries.set(entry.path, entry);
},
});
expect(entries.get('project/bin/postcheckout.sh')?.mode).toBe(0o755);
expect(entries.get('project/regular-file.txt')?.mode).toBe(0o644);
expect(entries.get('project/read-only-dir/')?.mode).toBe(0o755);
expect(entries.has('project/read-only-dir/child.txt')).toBe(true);
for (const entry of entries.values()) {
expect(entry.path.startsWith('project/')).toBe(true);
expect(entry.uid).toBeUndefined();
expect(entry.gid).toBeUndefined();
expect(entry.uname).toBe('');
expect(entry.gname).toBe('');
}
} finally {
await fs.remove(projectTarball.path);
removeSpy.mockRestore();
}
});
});
+3 -10
View File
@@ -143,16 +143,9 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
try {
await vcsClient.makeShallowCopyAsync(shallowClonePath);
await tar.create(
{
cwd: shallowClonePath,
file: tarPath,
prefix: 'project',
gzip: true,
portable: true,
},
['.']
);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
} catch (err) {
clearTimeout(timer);
if (spinner.isSpinning) {
@@ -0,0 +1,231 @@
import {
BUN_LOCK_FILE,
BUN_TEXT_LOCK_FILE,
NPM_LOCK_FILE,
PNPM_LOCK_FILE,
YARN_LOCK_FILE,
resolveWorkspaceRoot,
} from '@expo/package-manager';
import { Platform } from '@expo/eas-build-job';
import {
LockfileData,
findLockfileMismatches,
findPeerDependencyConflicts,
parseLockfile,
} from '@expo/lockfile-utils';
import fs from 'fs-extra';
import path from 'path';
import { CommonContext } from './context';
import Log from '../log';
const SKIP_HINT =
'To skip this check, set the EAS_BUILD_SKIP_LOCKFILE_CHECK=1 environment variable.';
interface LockfileInfo {
/** Filename of the lockfile (e.g. 'yarn.lock') */
filename: string;
/** Package manager name for display (e.g. 'yarn') */
managerName: string;
}
const LOCKFILE_BY_MANAGER: Record<string, LockfileInfo> = {
npm: { filename: NPM_LOCK_FILE, managerName: 'npm' },
yarn: { filename: YARN_LOCK_FILE, managerName: 'yarn' },
pnpm: { filename: PNPM_LOCK_FILE, managerName: 'pnpm' },
bun: { filename: BUN_LOCK_FILE, managerName: 'bun' },
};
const ALL_LOCKFILES: { filename: string; manager: string }[] = [
{ filename: NPM_LOCK_FILE, manager: 'npm' },
{ filename: YARN_LOCK_FILE, manager: 'yarn' },
{ filename: PNPM_LOCK_FILE, manager: 'pnpm' },
{ filename: BUN_LOCK_FILE, manager: 'bun' },
{ filename: BUN_TEXT_LOCK_FILE, manager: 'bun' },
];
/**
* Find the lockfile for the given filename by checking the workspace root first
* (for monorepos), then the project directory.
*/
async function findLockfileAsync(
projectDir: string,
filename: string
): Promise<string | null> {
const workspaceRoot = resolveWorkspaceRoot(projectDir);
if (workspaceRoot) {
const workspaceLockfile = path.join(workspaceRoot, filename);
if (await fs.pathExists(workspaceLockfile)) {
return workspaceLockfile;
}
}
const projectLockfile = path.join(projectDir, filename);
if (await fs.pathExists(projectLockfile)) {
return projectLockfile;
}
return null;
}
async function readLockfileDataAsync(
projectDir: string,
lockfilePath: string,
manager: string
): Promise<{ lockfileData: LockfileData; packageJson: Record<string, unknown> } | null> {
const packageJsonPath = path.join(projectDir, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
return null;
}
const packageJson = await fs.readJson(packageJsonPath);
const lockfileContent = await fs.readFile(lockfilePath, 'utf-8');
const lockfileData = parseLockfile(lockfileContent, manager, lockfilePath);
if (!lockfileData) {
return null;
}
return { lockfileData, packageJson };
}
/**
* Pre-flight lockfile validation for `eas build`. Catches common lockfile
* problems before uploading to the builder.
*/
export async function checkLockfileAsync<T extends Platform>(
ctx: CommonContext<T>
): Promise<void> {
if (process.env.EAS_BUILD_SKIP_LOCKFILE_CHECK) {
Log.warn('Skipping lockfile validation (EAS_BUILD_SKIP_LOCKFILE_CHECK is set).');
return;
}
const { projectDir, requiredPackageManager, vcsClient } = ctx;
// --- 1. Lockfile existence ---
if (!requiredPackageManager) {
Log.error('No lockfile found in the project directory.');
Log.error(
'A lockfile is required to ensure deterministic dependency installation on the EAS builder.'
);
Log.error(
'Run your package manager\'s install command (e.g. "npm install") to generate one, then commit it to version control.'
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
const lockInfo = LOCKFILE_BY_MANAGER[requiredPackageManager];
if (!lockInfo) {
// Unknown package manager — nothing we can validate
return;
}
// For bun, also check the text lockfile variant
const lockfilePath =
requiredPackageManager === 'bun'
? (await findLockfileAsync(projectDir, BUN_LOCK_FILE)) ??
(await findLockfileAsync(projectDir, BUN_TEXT_LOCK_FILE))
: await findLockfileAsync(projectDir, lockInfo.filename);
if (!lockfilePath) {
Log.error(
`${lockInfo.managerName} lockfile (${lockInfo.filename}) not found.`
);
Log.error(
`Run "${requiredPackageManager} install" to generate a lockfile, then commit it to version control.`
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
// --- 2. Lockfile tracked in VCS ---
const rootDir = path.normalize(await vcsClient.getRootPathAsync());
const relativeLockfilePath = path.relative(rootDir, lockfilePath);
if (await vcsClient.isFileIgnoredAsync(relativeLockfilePath)) {
Log.error(
`${lockInfo.managerName} lockfile (${relativeLockfilePath}) is ignored by your version control system and won't be uploaded to the EAS builder.`
);
Log.error(
'Remove the lockfile from .gitignore (and .easignore, if applicable), then commit it.'
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
// --- 3. Conflicting lockfiles ---
const foundManagers = new Set<string>();
for (const { filename, manager } of ALL_LOCKFILES) {
if (await findLockfileAsync(projectDir, filename)) {
foundManagers.add(manager);
}
}
if (foundManagers.size > 1) {
const managerList = [...foundManagers].sort().join(', ');
Log.error(`Conflicting lockfiles detected from multiple package managers: ${managerList}.`);
Log.error(
'Having lockfiles from multiple package managers can cause install failures on the EAS builder. Remove the lockfiles you do not need and commit the change.'
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
// --- 4 & 5. Parse lockfile for dependency sync and peer dep checks ---
// For bun, prefer the text lockfile for parsing (bun.lockb is binary)
const parsableLockfilePath =
requiredPackageManager === 'bun'
? (await findLockfileAsync(projectDir, BUN_TEXT_LOCK_FILE)) ?? lockfilePath
: lockfilePath;
const parsed = await readLockfileDataAsync(
projectDir,
parsableLockfilePath,
requiredPackageManager
);
if (!parsed) {
return; // Can't parse — skip remaining checks
}
// --- 4. Lockfile out of sync ---
const mismatches = findLockfileMismatches(parsed.packageJson, parsed.lockfileData);
if (mismatches.length > 0) {
Log.error('Lockfile is out of sync with package.json:');
for (const m of mismatches) {
Log.error(
` ${m.name}: package.json requires ${m.specifier}, but lockfile has ${m.lockedVersion}`
);
}
Log.error(
`Run "${requiredPackageManager} install" to update your lockfile, then commit the changes.`
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
// --- 5. Peer dependency conflicts ---
const peerConflicts = findPeerDependencyConflicts(parsed.packageJson, parsed.lockfileData);
if (peerConflicts.length > 0) {
Log.error('Peer dependency conflicts detected:');
for (const c of peerConflicts) {
Log.error(
` ${c.source}@${c.sourceVersion} requires peer ${c.peer}@"${c.peerRange}", but ${c.peer}@${c.installedVersion} is installed`
);
}
Log.error(
'Update the conflicting packages to compatible versions, then run ' +
`"${requiredPackageManager} install" and commit the changes.`
);
Log.error(SKIP_HINT);
process.exit(1);
return;
}
}
@@ -1,126 +0,0 @@
import {
confirmRecentConvexInviteAsync,
formatConvexProject,
formatConvexTeam,
formatConvexTeamConnection,
getConvexProjectDashboardUrl,
getConvexTeamDashboardUrl,
} from '../convex';
import {
ConvexProjectData,
ConvexTeamConnectionData,
} from '../../graphql/types/ConvexTeamConnection';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
jest.mock('../../log');
jest.mock('../../prompts');
describe('Convex command utilities', () => {
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: '2024-01-02T00:00:00.000Z',
invitedEmail: '[email protected]',
};
const mockProject: ConvexProjectData = {
id: 'convex-project-1',
convexProjectIdentifier: 'project-123',
convexProjectName: 'Test Project',
convexProjectSlug: 'test project',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
convexTeamConnection: mockConnection,
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('builds encoded Convex dashboard URLs', () => {
expect(getConvexTeamDashboardUrl(mockConnection)).toBe(
'https://dashboard.convex.dev/t/test%20team'
);
expect(getConvexProjectDashboardUrl(mockProject)).toBe(
'https://dashboard.convex.dev/t/test%20team/test%20project'
);
});
it('formats team and project metadata with invite details', () => {
expect(formatConvexTeam(mockConnection)).toBe('Test Team / test team');
expect(formatConvexTeamConnection(mockConnection)).toContain('Test Team / test team');
expect(formatConvexTeamConnection(mockConnection)).not.toContain('team-123');
expect(formatConvexTeamConnection(mockConnection)).not.toContain('connection-1');
expect(formatConvexTeamConnection(mockConnection)).toContain('[email protected]');
expect(formatConvexTeamConnection(mockConnection)).toContain('2024-01-02T00:00:00.000Z');
expect(formatConvexProject(mockProject)).toContain('project-123');
expect(formatConvexProject(mockProject)).not.toContain('convex-project-1');
expect(formatConvexProject(mockProject)).toContain('Test Team / test team');
expect(formatConvexProject(mockProject)).not.toContain('team-123');
});
it('does not prompt when the previous invite timestamp is invalid', async () => {
await expect(
confirmRecentConvexInviteAsync(
{ ...mockConnection, invitedAt: 'not-a-date' },
{ nonInteractive: false }
)
).resolves.toBe(true);
expect(confirmAsync).not.toHaveBeenCalled();
});
it('does not prompt when the previous invite is old', async () => {
await expect(
confirmRecentConvexInviteAsync(
{
...mockConnection,
invitedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
},
{ nonInteractive: false }
)
).resolves.toBe(true);
expect(confirmAsync).not.toHaveBeenCalled();
});
it('prompts before resending a recent invite in interactive mode', async () => {
jest.mocked(confirmAsync).mockResolvedValue(false);
await expect(
confirmRecentConvexInviteAsync(
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
{ nonInteractive: false }
)
).resolves.toBe(false);
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('Are you sure you want to send another invite?'),
});
});
it('warns and proceeds for recent invites in non-interactive mode', async () => {
await expect(
confirmRecentConvexInviteAsync(
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
{ nonInteractive: true }
)
).resolves.toBe(true);
expect(confirmAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(expect.stringContaining('non-interactive mode'));
});
});
@@ -1,83 +0,0 @@
import chalk from 'chalk';
import { ConvexProjectData, ConvexTeamConnectionData } from '../graphql/types/ConvexTeamConnection';
import Log, { link } from '../log';
import { confirmAsync } from '../prompts';
const CONVEX_DASHBOARD_HOST = 'https://dashboard.convex.dev';
const RECENT_INVITE_THRESHOLD_MS = 60 * 60 * 1000;
export function getConvexTeamDashboardUrl(connection: ConvexTeamConnectionData): string {
return `${CONVEX_DASHBOARD_HOST}/t/${encodeURIComponent(connection.convexTeamSlug)}`;
}
export function getConvexProjectDashboardUrl(project: ConvexProjectData): string {
return `${getConvexTeamDashboardUrl(project.convexTeamConnection)}/${encodeURIComponent(
project.convexProjectSlug
)}`;
}
export function formatConvexTeam(connection: ConvexTeamConnectionData): string {
return `${connection.convexTeamName} / ${connection.convexTeamSlug}`;
}
export function formatConvexTeamConnection(connection: ConvexTeamConnectionData): string {
const lines = [
`${chalk.bold('Team')}: ${formatConvexTeam(connection)}`,
`${chalk.bold('Dashboard')}: ${link(getConvexTeamDashboardUrl(connection), { dim: false })}`,
];
if (connection.invitedEmail) {
lines.push(`${chalk.bold('Invited email')}: ${connection.invitedEmail}`);
}
if (connection.invitedAt) {
lines.push(`${chalk.bold('Invited at')}: ${connection.invitedAt}`);
}
return lines.join('\n');
}
export function formatConvexProject(project: ConvexProjectData): string {
return [
`${chalk.bold('Name')}: ${project.convexProjectName}`,
`${chalk.bold('Slug')}: ${project.convexProjectSlug}`,
`${chalk.bold('Identifier')}: ${project.convexProjectIdentifier}`,
`${chalk.bold('Team')}: ${formatConvexTeam(project.convexTeamConnection)}`,
`${chalk.bold('Dashboard')}: ${link(getConvexProjectDashboardUrl(project), { dim: false })}`,
].join('\n');
}
export function logNoConvexTeams(accountName: string): void {
Log.warn(`No Convex team is linked to account ${chalk.bold(accountName)} on EAS.`);
}
export function logNoConvexProject(projectName: string): void {
Log.warn(`No Convex project is linked to Expo app ${chalk.bold(projectName)} on EAS.`);
}
export async function confirmRecentConvexInviteAsync(
connection: ConvexTeamConnectionData,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<boolean> {
const invitedAt = connection.invitedAt ? new Date(connection.invitedAt) : null;
if (!invitedAt || Number.isNaN(invitedAt.getTime())) {
return true;
}
const timeSinceInviteMs = Date.now() - invitedAt.getTime();
if (timeSinceInviteMs >= RECENT_INVITE_THRESHOLD_MS) {
return true;
}
const previousInvite = `A Convex team invite was already sent${connection.invitedEmail ? ` to ${connection.invitedEmail}` : ''} at ${connection.invitedAt}.`;
if (nonInteractive) {
Log.warn(
`${previousInvite} Sending another invite because this command is running in non-interactive mode.`
);
return true;
}
return await confirmAsync({
message: `${previousInvite} Are you sure you want to send another invite?`,
});
}
@@ -1,293 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import * as fs from 'fs-extra';
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import {
ConvexTeamConnectionData,
SetupConvexProjectResultData,
} from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../../../prompts';
import { Actor } from '../../../../user/User';
import IntegrationsConvexConnect from '../connect';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../graphql/mutations/ConvexMutation');
jest.mock('../../../../project/projectUtils');
jest.mock('../../../../prompts');
jest.mock('@expo/spawn-async');
jest.mock('fs-extra');
jest.mock('../../../../log');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
}),
}));
describe(IntegrationsConvexConnect, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const testProjectDir = '/test/project';
const testAccountId = 'test-account-id';
const testAccountName = 'testuser';
const mockActor: Actor = {
__typename: 'User',
id: 'test-user-id',
username: testAccountName,
email: '[email protected]',
featureGates: {},
isExpoAdmin: false,
primaryAccount: {
id: testAccountId,
name: testAccountName,
ownerUserActor: null,
users: [],
},
preferences: { onboarding: null },
accounts: [],
};
const mockRobotActor: Actor = {
__typename: 'Robot',
id: 'robot-1',
featureGates: {},
isExpoAdmin: false,
accounts: [],
};
const mockAccount = {
id: testAccountId,
name: testAccountName,
ownerUserActor: { id: 'test-user-id', username: testAccountName },
users: [{ role: 'OWNER' as any, actor: { id: 'test-user-id' } }],
};
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: null,
invitedEmail: null,
};
const mockSetupResult: SetupConvexProjectResultData = {
convexDeploymentName: 'happy-otter-123',
convexDeploymentUrl: 'https://happy-otter-123.convex.cloud',
deployKey: 'dev:happy-otter-123|abc123token',
convexProject: {
id: 'convex-project-1',
convexProjectIdentifier: 'project-123',
convexProjectName: 'testapp',
convexProjectSlug: 'testapp',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
convexTeamConnection: mockConnection,
},
};
function createCommand(argv: string[], actor: Actor = mockActor): IntegrationsConvexConnect {
const command = new IntegrationsConvexConnect(argv, mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
exp: { name: 'testapp', slug: 'testapp' },
projectDir: testProjectDir,
},
loggedIn: { graphqlClient, actor },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'withTick').mockImplementation(() => {});
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
jest.spyOn(Log, 'newLine').mockImplementation(() => {});
jest.mocked(fs.pathExists).mockResolvedValue(false as never);
jest.mocked(fs.writeFile).mockResolvedValue(undefined as never);
jest.mocked(fs.readFile).mockResolvedValue('' as never);
jest.mocked(spawnAsync).mockResolvedValue({} as never);
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockAccount as any);
jest.mocked(selectAsync).mockResolvedValue('aws-us-east-1');
jest.mocked(confirmAsync).mockResolvedValue(true);
jest.mocked(promptAsync).mockImplementation(async (params: any) => ({
[params.name]: params.initial,
}));
jest.mocked(ConvexMutation.createConvexTeamConnectionAsync).mockResolvedValue(mockConnection);
jest.mocked(ConvexMutation.setupConvexProjectAsync).mockResolvedValue(mockSetupResult);
jest.mocked(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).mockResolvedValue(true);
});
it('creates a Convex team and project with the new mutation shape', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([]);
await createCommand([]).runAsync();
expect(ConvexMutation.createConvexTeamConnectionAsync).toHaveBeenCalledWith(graphqlClient, {
accountId: testAccountId,
deploymentRegion: 'aws-us-east-1',
convexTeamName: testAccountName,
});
expect(ConvexMutation.setupConvexProjectAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
convexTeamConnectionId: 'connection-1',
deploymentRegion: 'aws-us-east-1',
projectName: 'testapp',
});
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalledWith(
graphqlClient,
{ convexTeamConnectionId: 'connection-1' }
);
expect(spawnAsync).toHaveBeenCalledWith('npx', ['expo', 'install', 'convex'], {
cwd: testProjectDir,
stdio: 'inherit',
});
});
it('uses an existing Convex team connection', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
await createCommand(['--region', 'aws-eu-west-1']).runAsync();
expect(ConvexMutation.createConvexTeamConnectionAsync).not.toHaveBeenCalled();
expect(ConvexMutation.setupConvexProjectAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
convexTeamConnectionId: 'connection-1',
deploymentRegion: 'aws-eu-west-1',
projectName: 'testapp',
});
expect(spawnAsync).toHaveBeenCalledWith('npx', ['expo', 'install', 'convex'], {
cwd: testProjectDir,
stdio: 'inherit',
});
});
it('does not create a Convex team if installing the Convex package fails first', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([]);
jest.mocked(spawnAsync).mockRejectedValue(new Error('install failed') as never);
await expect(createCommand([]).runAsync()).rejects.toThrow('install failed');
expect(ConvexMutation.createConvexTeamConnectionAsync).not.toHaveBeenCalled();
expect(ConvexMutation.setupConvexProjectAsync).not.toHaveBeenCalled();
});
it('skips the verified email invite when the actor has no email', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
await createCommand([], mockRobotActor).runAsync();
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('Could not determine your verified email address')
);
expect(Log.warn).not.toHaveBeenCalledWith(expect.stringContaining('Convex dashboard'));
});
it('logs the error when sending a verified email invite fails', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
jest
.mocked(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync)
.mockRejectedValue(new Error('Convex invite service unavailable'));
await createCommand([]).runAsync();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('Failed to send Convex team invitation to [email protected]')
);
expect(Log.warn).toHaveBeenCalledWith('Convex invite service unavailable');
expect(Log.warn).not.toHaveBeenCalledWith(expect.stringContaining('Convex dashboard'));
});
it('asks before resending a recent team invite for an existing connection', async () => {
jest.mocked(confirmAsync).mockResolvedValue(false);
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
invitedEmail: '[email protected]',
},
]);
await createCommand([]).runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('Are you sure you want to send another invite?'),
});
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith('Skipped sending Convex team invitation.');
});
it('writes the deploy key and Convex URL to .env.local', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
await createCommand([]).runAsync();
const envPath = `${testProjectDir}/.env.local`;
const writeCall = jest.mocked(fs.writeFile).mock.calls.find(call => call[0] === envPath);
expect(writeCall?.[1]).toContain('CONVEX_DEPLOY_KEY=dev:happy-otter-123|abc123token');
expect(writeCall?.[1]).toContain('EXPO_PUBLIC_CONVEX_URL=https://happy-otter-123.convex.cloud');
});
it('prompts before overwriting existing Convex .env.local values', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
jest.mocked(fs.pathExists).mockResolvedValue(true as never);
jest.mocked(fs.readFile).mockResolvedValue('CONVEX_DEPLOY_KEY=old-key\n' as never);
jest.mocked(confirmAsync).mockResolvedValue(true);
await createCommand([]).runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('already contains CONVEX_DEPLOY_KEY'),
});
});
it('uses defaults in non-interactive mode', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([]);
await createCommand(['--non-interactive']).runAsync();
expect(selectAsync).not.toHaveBeenCalled();
expect(promptAsync).not.toHaveBeenCalled();
expect(ConvexMutation.createConvexTeamConnectionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({
deploymentRegion: 'aws-us-east-1',
convexTeamName: testAccountName,
})
);
expect(ConvexMutation.setupConvexProjectAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({
appId: testProjectId,
deploymentRegion: 'aws-us-east-1',
projectName: 'testapp',
})
);
});
});
@@ -1,125 +0,0 @@
import openBrowserAsync from 'better-opn';
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import {
ConvexProjectData,
ConvexTeamConnectionData,
} from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { ora } from '../../../../ora';
import IntegrationsConvexDashboard from '../dashboard';
jest.mock('better-opn');
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../log');
jest.mock('../../../../ora', () => ({
ora: jest.fn(() => ({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
})),
}));
describe(IntegrationsConvexDashboard, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: null,
invitedEmail: null,
};
const mockProject: ConvexProjectData = {
id: 'convex-project-1',
convexProjectIdentifier: 'project-123',
convexProjectName: 'Test Project',
convexProjectSlug: 'test project',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
convexTeamConnection: mockConnection,
};
function createCommand(): IntegrationsConvexDashboard {
const command = new IntegrationsConvexDashboard([], mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
exp: { slug: 'testapp' },
},
loggedIn: { graphqlClient },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(mockProject);
jest.mocked(openBrowserAsync).mockResolvedValue({} as never);
jest.mocked(ora).mockReturnValue({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
} as any);
});
it('opens the linked Convex project dashboard', async () => {
await createCommand().runAsync();
expect(openBrowserAsync).toHaveBeenCalledWith(
'https://dashboard.convex.dev/t/test%20team/test%20project'
);
});
it('logs an empty state when no project link exists', async () => {
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(null);
await createCommand().runAsync();
expect(openBrowserAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('No Convex project is linked to Expo app')
);
});
it('fails the spinner when the browser cannot be opened', async () => {
jest.mocked(openBrowserAsync).mockResolvedValue(false);
const spinner = {
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
};
jest.mocked(ora).mockReturnValue(spinner as any);
await createCommand().runAsync();
expect(spinner.fail).toHaveBeenCalledWith(
expect.stringContaining('Unable to open a web browser')
);
});
it('fails the spinner and rethrows when opening the browser throws', async () => {
jest.mocked(openBrowserAsync).mockRejectedValue(new Error('open failed'));
const spinner = {
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
};
jest.mocked(ora).mockReturnValue(spinner as any);
await expect(createCommand().runAsync()).rejects.toThrow('open failed');
expect(spinner.fail).toHaveBeenCalledWith(
expect.stringContaining('Unable to open a web browser')
);
});
});
@@ -1,120 +0,0 @@
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import {
ConvexProjectData,
ConvexTeamConnectionData,
} from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { confirmAsync } from '../../../../prompts';
import IntegrationsConvexProjectDelete from '../project/delete';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../graphql/mutations/ConvexMutation');
jest.mock('../../../../prompts');
jest.mock('../../../../log');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
}),
}));
describe(IntegrationsConvexProjectDelete, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: null,
invitedEmail: null,
};
const mockProject: ConvexProjectData = {
id: 'convex-project-1',
convexProjectIdentifier: 'project-123',
convexProjectName: 'Test Project',
convexProjectSlug: 'test-project',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
convexTeamConnection: mockConnection,
};
function createCommand(argv: string[]): IntegrationsConvexProjectDelete {
const command = new IntegrationsConvexProjectDelete(argv, mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
exp: { slug: 'testapp' },
},
loggedIn: { graphqlClient },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'error').mockImplementation(() => {});
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
jest.spyOn(Log, 'newLine').mockImplementation(() => {});
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(mockProject);
jest.mocked(ConvexMutation.deleteConvexProjectAsync).mockResolvedValue('convex-project-1');
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('deletes the linked Convex project after confirmation', async () => {
await createCommand([]).runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('This does not destroy resources on Convex'),
});
expect(ConvexMutation.deleteConvexProjectAsync).toHaveBeenCalledWith(
graphqlClient,
'convex-project-1'
);
});
it('skips deletion when the user cancels', async () => {
jest.mocked(confirmAsync).mockResolvedValue(false);
await createCommand([]).runAsync();
expect(ConvexMutation.deleteConvexProjectAsync).not.toHaveBeenCalled();
expect(Log.error).toHaveBeenCalledWith('Canceled deletion of the Convex project link');
});
it('uses --yes to skip the confirmation prompt', async () => {
await createCommand(['--yes']).runAsync();
expect(confirmAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('This does not destroy resources on Convex')
);
expect(ConvexMutation.deleteConvexProjectAsync).toHaveBeenCalledWith(
graphqlClient,
'convex-project-1'
);
});
it('logs an empty state when no project link exists', async () => {
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(null);
await createCommand([]).runAsync();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('No Convex project is linked to Expo app')
);
expect(ConvexMutation.deleteConvexProjectAsync).not.toHaveBeenCalled();
});
});
@@ -1,78 +0,0 @@
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import {
ConvexProjectData,
ConvexTeamConnectionData,
} from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import IntegrationsConvexProject from '../project';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../log');
describe(IntegrationsConvexProject, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: null,
invitedEmail: null,
};
const mockProject: ConvexProjectData = {
id: 'convex-project-1',
convexProjectIdentifier: 'project-123',
convexProjectName: 'Test Project',
convexProjectSlug: 'test-project',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
convexTeamConnection: mockConnection,
};
function createCommand(): IntegrationsConvexProject {
const command = new IntegrationsConvexProject([], mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
exp: { slug: 'testapp' },
},
loggedIn: { graphqlClient },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
});
it('prints linked Convex project metadata', async () => {
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(mockProject);
await createCommand().runAsync();
expect(Log.log).toHaveBeenCalledWith(
expect.stringContaining('Convex project linked to testapp')
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('project-123'));
});
it('prints an empty state when no Convex project is linked', async () => {
jest.mocked(ConvexQuery.getConvexProjectByAppIdAsync).mockResolvedValue(null);
await createCommand().runAsync();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('No Convex project is linked to Expo app')
);
});
});
@@ -1,187 +0,0 @@
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../../../prompts';
import IntegrationsConvexTeamDelete from '../team/delete';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../graphql/mutations/ConvexMutation');
jest.mock('../../../../project/projectUtils');
jest.mock('../../../../prompts');
jest.mock('../../../../log');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
}),
}));
describe(IntegrationsConvexTeamDelete, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const testAccountId = 'test-account-id';
const testAccountName = 'testuser';
const mockAccount = {
id: testAccountId,
name: testAccountName,
ownerUserActor: { id: 'test-user-id', username: testAccountName },
users: [{ role: 'OWNER' as any, actor: { id: 'test-user-id' } }],
};
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: '2024-01-02T00:00:00.000Z',
invitedEmail: '[email protected]',
};
function createCommand(argv: string[]): IntegrationsConvexTeamDelete {
const command = new IntegrationsConvexTeamDelete(argv, mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
},
loggedIn: { graphqlClient },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'error').mockImplementation(() => {});
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
jest.spyOn(Log, 'newLine').mockImplementation(() => {});
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockAccount as any);
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
jest.mocked(ConvexMutation.deleteConvexTeamConnectionAsync).mockResolvedValue(mockConnection);
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('deletes the selected Convex team link after confirmation', async () => {
await createCommand(['test-team']).runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('This does not destroy resources on Convex'),
});
expect(ConvexMutation.deleteConvexTeamConnectionAsync).toHaveBeenCalledWith(
graphqlClient,
'connection-1'
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Test Team / test-team'));
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('team-123'));
});
it('skips deletion when the user cancels', async () => {
jest.mocked(confirmAsync).mockResolvedValue(false);
await createCommand(['test-team']).runAsync();
expect(ConvexMutation.deleteConvexTeamConnectionAsync).not.toHaveBeenCalled();
expect(Log.error).toHaveBeenCalledWith('Canceled deletion of the Convex team link');
});
it('uses --yes to skip the confirmation prompt', async () => {
await createCommand(['test-team', '--yes']).runAsync();
expect(confirmAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('This does not destroy resources on Convex')
);
expect(ConvexMutation.deleteConvexTeamConnectionAsync).toHaveBeenCalledWith(
graphqlClient,
'connection-1'
);
});
it('logs an empty state when no team links exist', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([]);
await createCommand([]).runAsync();
expect(Log.warn).toHaveBeenCalledWith(expect.stringContaining('No Convex team is linked'));
expect(ConvexMutation.deleteConvexTeamConnectionAsync).not.toHaveBeenCalled();
});
it('throws when the provided team is not linked to the account', async () => {
await expect(createCommand(['missing-team']).runAsync()).rejects.toThrow(
'Convex team missing-team is not linked to this account.'
);
expect(ConvexMutation.deleteConvexTeamConnectionAsync).not.toHaveBeenCalled();
});
it('resolves a team by slug when multiple links exist', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
mockConnection,
{
...mockConnection,
id: 'connection-2',
convexTeamIdentifier: 'team-456',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
},
]);
await createCommand(['other-team', '--non-interactive', '--yes']).runAsync();
expect(ConvexMutation.deleteConvexTeamConnectionAsync).toHaveBeenCalledWith(
graphqlClient,
'connection-2'
);
});
it('requires a team slug in non-interactive mode when multiple links exist', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
mockConnection,
{
...mockConnection,
id: 'connection-2',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
},
]);
await expect(createCommand(['--non-interactive']).runAsync()).rejects.toThrow(
'Convex team slug must be provided'
);
});
it('prompts to select a team link when multiple links exist in interactive mode', async () => {
const otherConnection = {
...mockConnection,
id: 'connection-2',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
};
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection, otherConnection]);
jest.mocked(selectAsync).mockResolvedValue(otherConnection);
await createCommand(['--yes']).runAsync();
expect(selectAsync).toHaveBeenCalledWith(
'Select a Convex team link to remove',
expect.any(Array)
);
expect(ConvexMutation.deleteConvexTeamConnectionAsync).toHaveBeenCalledWith(
graphqlClient,
'connection-2'
);
});
});
@@ -1,220 +0,0 @@
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../../../prompts';
import IntegrationsConvexTeamInvite from '../team/invite';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../graphql/mutations/ConvexMutation');
jest.mock('../../../../project/projectUtils');
jest.mock('../../../../prompts');
jest.mock('../../../../log');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
}),
}));
describe(IntegrationsConvexTeamInvite, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const testAccountId = 'test-account-id';
const testAccountName = 'testuser';
const mockActor = {
__typename: 'User',
id: 'test-user-id',
email: '[email protected]',
accounts: [],
};
const mockAccount = {
id: testAccountId,
name: testAccountName,
ownerUserActor: { id: 'test-user-id', username: testAccountName },
users: [{ role: 'OWNER' as any, actor: { id: 'test-user-id' } }],
};
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: '2024-01-02T00:00:00.000Z',
invitedEmail: '[email protected]',
};
function createCommand(
argv: string[],
actor: { __typename: string; [key: string]: any } = mockActor
): IntegrationsConvexTeamInvite {
const command = new IntegrationsConvexTeamInvite(argv, mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
},
loggedIn: { graphqlClient, actor },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
jest.spyOn(Log, 'newLine').mockImplementation(() => {});
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockAccount as any);
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
jest.mocked(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).mockResolvedValue(true);
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('sends an invite for the linked Convex team', async () => {
await createCommand([]).runAsync();
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalledWith(
graphqlClient,
{ convexTeamConnectionId: 'connection-1' }
);
});
it('prints previous invite metadata when it exists', async () => {
await createCommand([]).runAsync();
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Test Team / test-team'));
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('team-123'));
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('connection-1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Previous invite'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('[email protected]'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('2024-01-02T00:00:00.000Z'));
});
it('prompts before resending a recent invite', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
]);
await createCommand([]).runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: expect.stringContaining('Are you sure you want to send another invite?'),
});
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalled();
});
it('skips resending a recent invite when the user does not confirm', async () => {
jest.mocked(confirmAsync).mockResolvedValue(false);
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
]);
await createCommand([]).runAsync();
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith('Skipped sending Convex team invitation.');
});
it('resends recent invites in non-interactive mode with a warning', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
{
...mockConnection,
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
]);
await createCommand(['--non-interactive']).runAsync();
expect(confirmAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(expect.stringContaining('non-interactive mode'));
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalled();
});
it('skips the invite when the actor has no email', async () => {
await createCommand([], { __typename: 'Robot', id: 'robot-1' }).runAsync();
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('Could not determine your verified email address')
);
expect(Log.warn).not.toHaveBeenCalledWith(expect.stringContaining('Convex dashboard'));
});
it('selects a team link when multiple are available', async () => {
const otherConnection = {
...mockConnection,
id: 'connection-2',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
};
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection, otherConnection]);
jest.mocked(selectAsync).mockResolvedValue(otherConnection);
await createCommand([]).runAsync();
expect(selectAsync).toHaveBeenCalledWith(
'Select a Convex team link to invite yourself to',
expect.any(Array)
);
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalledWith(
graphqlClient,
{ convexTeamConnectionId: 'connection-2' }
);
});
it('resolves a team by slug when multiple links exist', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
mockConnection,
{
...mockConnection,
id: 'connection-2',
convexTeamIdentifier: 'team-456',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
},
]);
await createCommand(['other-team', '--non-interactive']).runAsync();
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).toHaveBeenCalledWith(
graphqlClient,
{ convexTeamConnectionId: 'connection-2' }
);
});
it('requires a team slug in non-interactive mode when multiple links exist', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
mockConnection,
{
...mockConnection,
id: 'connection-2',
convexTeamName: 'Other Team',
convexTeamSlug: 'other-team',
},
]);
await expect(createCommand(['--non-interactive']).runAsync()).rejects.toThrow(
'Convex team slug must be provided'
);
});
});
@@ -1,78 +0,0 @@
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../../graphql/types/ConvexTeamConnection';
import Log from '../../../../log';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import IntegrationsConvexTeam from '../team';
jest.mock('../../../../graphql/queries/ConvexQuery');
jest.mock('../../../../project/projectUtils');
jest.mock('../../../../log');
describe(IntegrationsConvexTeam, () => {
const graphqlClient = {} as ExpoGraphqlClient;
const mockConfig = getMockOclifConfig();
const testAccountId = 'test-account-id';
const testAccountName = 'testuser';
const mockAccount = {
id: testAccountId,
name: testAccountName,
ownerUserActor: { id: 'test-user-id', username: testAccountName },
users: [{ role: 'OWNER' as any, actor: { id: 'test-user-id' } }],
};
const mockConnection: ConvexTeamConnectionData = {
id: 'connection-1',
convexTeamIdentifier: 'team-123',
convexTeamName: 'Test Team',
convexTeamSlug: 'test-team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
invitedAt: null,
invitedEmail: null,
};
function createCommand(): IntegrationsConvexTeam {
const command = new IntegrationsConvexTeam([], mockConfig);
jest.spyOn(command as any, 'getContextAsync').mockReturnValue({
privateProjectConfig: {
projectId: testProjectId,
},
loggedIn: { graphqlClient },
} as never);
return command;
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'newLine').mockImplementation(() => {});
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockAccount as any);
});
it('prints linked Convex team metadata', async () => {
jest
.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync)
.mockResolvedValue([mockConnection]);
await createCommand().runAsync();
expect(Log.log).toHaveBeenCalledWith(
expect.stringContaining('Convex teams linked to @testuser')
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Test Team / test-team'));
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('team-123'));
});
it('prints an empty state when no Convex teams are linked', async () => {
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([]);
await createCommand().runAsync();
expect(Log.warn).toHaveBeenCalledWith(expect.stringContaining('No Convex team is linked'));
});
});
@@ -1,350 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import dotenv from 'dotenv';
import * as fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../../commandUtils/EasCommand';
import { confirmRecentConvexInviteAsync, formatConvexTeam } from '../../../commandUtils/convex';
import { EASNonInteractiveFlag } from '../../../commandUtils/flags';
import { ConvexMutation } from '../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../graphql/types/ConvexTeamConnection';
import Log from '../../../log';
import { ora } from '../../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../../prompts';
import { Actor } from '../../../user/User';
const CONVEX_REGIONS = [
{ title: 'US East (aws-us-east-1)', value: 'aws-us-east-1' },
{ title: 'EU West (aws-eu-west-1)', value: 'aws-eu-west-1' },
];
const DEFAULT_REGION = 'aws-us-east-1';
export default class IntegrationsConvexConnect extends EasCommand {
static override description = 'connect Convex to your Expo project';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
static override flags = {
...EASNonInteractiveFlag,
region: Flags.string({
description: 'Convex deployment region (e.g. aws-us-east-1, aws-eu-west-1)',
options: CONVEX_REGIONS.map(r => r.value),
}),
'team-name': Flags.string({
description: 'Name for the new Convex team (defaults to EAS account name)',
}),
'project-name': Flags.string({
description: 'Name for the Convex project (defaults to app slug)',
}),
};
async runAsync(): Promise<void> {
const {
flags: {
region: regionFlag,
'team-name': teamNameFlag,
'project-name': projectNameFlag,
'non-interactive': nonInteractive,
},
} = await this.parse(IntegrationsConvexConnect);
const {
privateProjectConfig: { projectId, exp, projectDir },
loggedIn: { graphqlClient, actor },
} = await this.getContextAsync(IntegrationsConvexConnect, {
nonInteractive,
withServerSideEnvironment: null,
});
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
// 1. Check for existing Convex team connections
const existingConnections = await ConvexQuery.getConvexTeamConnectionsByAccountIdAsync(
graphqlClient,
account.id
);
const region = await this.resolveRegionAsync(regionFlag, nonInteractive);
let connection: ConvexTeamConnectionData | null = null;
let teamName: string | null = null;
if (existingConnections.length === 0) {
// 2a. No connection - resolve a team name and create it after local package install succeeds
teamName = await this.resolveTeamNameAsync(teamNameFlag, account.name, nonInteractive);
} else if (existingConnections.length === 1) {
// 2b. Single existing connection
connection = existingConnections[0];
Log.withTick(`Using existing Convex team ${chalk.bold(formatConvexTeam(connection))}`);
} else {
// 2c. Multiple connections - prompt to select
connection = await this.selectConnectionAsync(existingConnections, nonInteractive);
}
// 3. Resolve project name before project setup mutation
const projectName = await this.resolveProjectNameAsync(
projectNameFlag,
exp.slug,
nonInteractive
);
// 4. Install the Convex package before creating new server-side resources
await this.installConvexPackageAsync(projectDir);
if (!connection) {
const spinner = ora('Creating Convex team').start();
try {
connection = await ConvexMutation.createConvexTeamConnectionAsync(graphqlClient, {
accountId: account.id,
deploymentRegion: region,
convexTeamName: teamName ?? account.name,
});
spinner.succeed(`Created Convex team ${chalk.bold(formatConvexTeam(connection))}`);
} catch (error) {
spinner.fail('Failed to create Convex team');
throw error;
}
}
// 5. Set up Convex project
const spinner = ora('Setting up Convex project').start();
let setupResult;
try {
setupResult = await ConvexMutation.setupConvexProjectAsync(graphqlClient, {
appId: projectId,
convexTeamConnectionId: connection.id,
deploymentRegion: region,
projectName,
});
spinner.succeed(
`Created Convex project ${chalk.bold(projectName)} with deployment ${chalk.bold(setupResult.convexDeploymentName)}`
);
} catch (error) {
spinner.fail('Failed to set up Convex project');
throw error;
}
// 6. Send team invite (non-fatal)
await this.sendTeamInviteAsync(graphqlClient, connection, actor, { nonInteractive });
// 7. Write deploy key and URL to .env.local
await this.writeEnvLocalAsync(
projectDir,
setupResult.deployKey,
setupResult.convexDeploymentUrl,
nonInteractive
);
// 8. Success message
Log.addNewLineIfNone();
Log.log(chalk.green('Convex is ready!'));
Log.newLine();
Log.log('Next steps:');
Log.log(` 1. Start the Convex dev server: ${chalk.cyan('npx convex dev')}`);
Log.newLine();
if (this.getActorEmail(actor)) {
Log.log(
`Check your email for an invitation to join your Convex team. Accept it for full dashboard access.`
);
}
}
private async resolveRegionAsync(
flagValue: string | undefined,
nonInteractive: boolean
): Promise<string> {
if (flagValue) {
return flagValue;
}
if (nonInteractive) {
return DEFAULT_REGION;
}
return await selectAsync('Select a Convex deployment region', CONVEX_REGIONS);
}
private async resolveTeamNameAsync(
flagValue: string | undefined,
accountName: string,
nonInteractive: boolean
): Promise<string> {
if (flagValue) {
return flagValue;
}
if (nonInteractive) {
return accountName;
}
const { teamName } = await promptAsync({
type: 'text',
name: 'teamName',
message: 'Convex team name',
initial: accountName,
validate: (value: string) => (value.trim() ? true : 'Team name cannot be empty'),
});
return teamName;
}
private async resolveProjectNameAsync(
flagValue: string | undefined,
slug: string,
nonInteractive: boolean
): Promise<string> {
if (flagValue) {
return flagValue;
}
if (nonInteractive) {
return slug;
}
const { projectName } = await promptAsync({
type: 'text',
name: 'projectName',
message: 'Convex project name',
initial: slug,
validate: (value: string) => (value.trim() ? true : 'Project name cannot be empty'),
});
return projectName;
}
private async selectConnectionAsync(
connections: ConvexTeamConnectionData[],
nonInteractive: boolean
): Promise<ConvexTeamConnectionData> {
if (nonInteractive) {
return connections[0];
}
const choices = connections.map(c => ({
title: `${formatConvexTeam(c)} (created ${new Date(c.createdAt).toLocaleDateString()})`,
value: c,
}));
return await selectAsync('Select a Convex team connection', choices);
}
private getActorEmail(actor: Actor): string | null {
return actor.__typename === 'User' ? actor.email : null;
}
private async sendTeamInviteAsync(
graphqlClient: Parameters<typeof ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync>[0],
connection: ConvexTeamConnectionData,
actor: Actor,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
const email = this.getActorEmail(actor);
if (!email) {
Log.warn(
`Could not determine your verified email address, so no Convex team invitation was sent. Run ${chalk.cyan(
'eas integrations:convex:team:invite'
)} after signing in with a user account.`
);
return;
}
if (!(await confirmRecentConvexInviteAsync(connection, { nonInteractive }))) {
Log.warn('Skipped sending Convex team invitation.');
return;
}
try {
await ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync(graphqlClient, {
convexTeamConnectionId: connection.id,
});
Log.withTick(`Sent Convex team invitation to ${chalk.bold(email)}`);
} catch (error) {
Log.warn(
`Failed to send Convex team invitation to ${email}. Run ${chalk.cyan(
'eas integrations:convex:team:invite'
)} to retry.`
);
Log.warn(error instanceof Error ? error.message : String(error));
}
}
private async installConvexPackageAsync(projectDir: string): Promise<void> {
Log.newLine();
Log.log(`Running ${chalk.bold('npx expo install convex')}`);
Log.newLine();
try {
await spawnAsync('npx', ['expo', 'install', 'convex'], {
cwd: projectDir,
stdio: 'inherit',
});
Log.withTick(`Installed the ${chalk.bold('convex')} npm package`);
} catch (error) {
Log.warn(
`Failed to install the ${chalk.bold('convex')} npm package. Run ${chalk.cyan(
'npx expo install convex'
)} from your project directory, then run ${chalk.cyan('npx convex dev')}.`
);
throw error;
}
}
private async writeEnvLocalAsync(
projectDir: string,
deployKey: string,
convexUrl: string,
nonInteractive: boolean
): Promise<void> {
const envPath = path.join(projectDir, '.env.local');
let existingContent: Record<string, string> = {};
let rawContent = '';
if (await fs.pathExists(envPath)) {
rawContent = await fs.readFile(envPath, 'utf8');
existingContent = dotenv.parse(rawContent);
if (existingContent.CONVEX_DEPLOY_KEY && !nonInteractive) {
const overwrite = await confirmAsync({
message: `.env.local already contains CONVEX_DEPLOY_KEY. Overwrite Convex values?`,
});
if (!overwrite) {
Log.log('Skipping .env.local update. Deploy key:');
Log.log(` CONVEX_DEPLOY_KEY=${deployKey}`);
Log.log(` EXPO_PUBLIC_CONVEX_URL=${convexUrl}`);
return;
}
}
}
const updatedContent = this.mergeEnvContent(rawContent, {
CONVEX_DEPLOY_KEY: deployKey,
EXPO_PUBLIC_CONVEX_URL: convexUrl,
});
await fs.writeFile(envPath, updatedContent);
Log.withTick(`Wrote Convex config to ${chalk.bold('.env.local')}`);
}
private mergeEnvContent(rawContent: string, newVars: Record<string, string>): string {
let content = rawContent;
const keysToAdd: Record<string, string> = { ...newVars };
for (const [key, value] of Object.entries(newVars)) {
// Replace existing line if present
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(content)) {
content = content.replace(regex, `${key}=${value}`);
delete keysToAdd[key];
}
}
// Append any keys that weren't already in the file
const remaining = Object.entries(keysToAdd);
if (remaining.length > 0) {
if (content.length > 0 && !content.endsWith('\n')) {
content += '\n';
}
for (const [key, value] of remaining) {
content += `${key}=${value}\n`;
}
}
return content;
}
}
@@ -1,46 +0,0 @@
import openBrowserAsync from 'better-opn';
import EasCommand from '../../../commandUtils/EasCommand';
import { getConvexProjectDashboardUrl, logNoConvexProject } from '../../../commandUtils/convex';
import { ConvexQuery } from '../../../graphql/queries/ConvexQuery';
import { ora } from '../../../ora';
export default class IntegrationsConvexDashboard extends EasCommand {
static override description = 'open the Convex dashboard for the linked Convex project';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
privateProjectConfig: { projectId, exp },
loggedIn: { graphqlClient },
} = await this.getContextAsync(IntegrationsConvexDashboard, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const convexProject = await ConvexQuery.getConvexProjectByAppIdAsync(graphqlClient, projectId);
if (!convexProject) {
logNoConvexProject(exp.slug);
return;
}
const dashboardUrl = getConvexProjectDashboardUrl(convexProject);
const failedMessage = `Unable to open a web browser. Convex dashboard is available at: ${dashboardUrl}`;
const spinner = ora(`Opening ${dashboardUrl}`).start();
try {
const opened = await openBrowserAsync(dashboardUrl);
if (opened) {
spinner.succeed(`Opened ${dashboardUrl}`);
} else {
spinner.fail(failedMessage);
}
} catch (error) {
spinner.fail(failedMessage);
throw error;
}
}
}
@@ -1,34 +0,0 @@
import chalk from 'chalk';
import EasCommand from '../../../commandUtils/EasCommand';
import { formatConvexProject, logNoConvexProject } from '../../../commandUtils/convex';
import { ConvexQuery } from '../../../graphql/queries/ConvexQuery';
import Log from '../../../log';
export default class IntegrationsConvexProject extends EasCommand {
static override description = 'display the Convex project linked to the current Expo app';
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
privateProjectConfig: { projectId, exp },
loggedIn: { graphqlClient },
} = await this.getContextAsync(IntegrationsConvexProject, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const convexProject = await ConvexQuery.getConvexProjectByAppIdAsync(graphqlClient, projectId);
if (!convexProject) {
logNoConvexProject(exp.slug);
return;
}
Log.log(chalk.bold(`Convex project linked to ${exp.slug}`));
Log.log(formatConvexProject(convexProject));
}
}
@@ -1,87 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../../../commandUtils/EasCommand';
import {
formatConvexProject,
getConvexProjectDashboardUrl,
logNoConvexProject,
} from '../../../../commandUtils/convex';
import { EASNonInteractiveFlag } from '../../../../commandUtils/flags';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import Log, { link } from '../../../../log';
import { ora } from '../../../../ora';
import { confirmAsync } from '../../../../prompts';
export default class IntegrationsConvexProjectDelete extends EasCommand {
static override description =
'remove the Convex project link for the current Expo app from EAS servers';
static override flags = {
...EASNonInteractiveFlag,
yes: Flags.boolean({
char: 'y',
description: 'Skip confirmation prompt',
default: false,
}),
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
flags: { 'non-interactive': nonInteractive, yes },
} = await this.parse(IntegrationsConvexProjectDelete);
const {
privateProjectConfig: { projectId, exp },
loggedIn: { graphqlClient },
} = await this.getContextAsync(IntegrationsConvexProjectDelete, {
nonInteractive,
withServerSideEnvironment: null,
});
const convexProject = await ConvexQuery.getConvexProjectByAppIdAsync(graphqlClient, projectId);
if (!convexProject) {
logNoConvexProject(exp.slug);
return;
}
Log.addNewLineIfNone();
Log.log(formatConvexProject(convexProject));
Log.newLine();
const dashboardUrl = getConvexProjectDashboardUrl(convexProject);
if (!nonInteractive && !yes) {
const confirmed = await confirmAsync({
message: `Remove this Convex project link from EAS servers? This does not destroy resources on Convex. Convex dashboard: ${link(
dashboardUrl,
{ dim: false }
)}`,
});
if (!confirmed) {
Log.error('Canceled deletion of the Convex project link');
return;
}
} else {
Log.warn(
`Removing the Convex project link from EAS servers. This does not destroy resources on Convex: ${dashboardUrl}`
);
}
const spinner = ora('Removing Convex project link').start();
try {
await ConvexMutation.deleteConvexProjectAsync(graphqlClient, convexProject.id);
spinner.succeed(
`Removed Convex project ${chalk.bold(convexProject.convexProjectName)} from EAS servers`
);
} catch (error) {
spinner.fail('Failed to remove Convex project link');
throw error;
}
}
}
@@ -1,45 +0,0 @@
import chalk from 'chalk';
import EasCommand from '../../../commandUtils/EasCommand';
import { formatConvexTeamConnection, logNoConvexTeams } from '../../../commandUtils/convex';
import { ConvexQuery } from '../../../graphql/queries/ConvexQuery';
import Log from '../../../log';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
export default class IntegrationsConvexTeam extends EasCommand {
static override description =
"display Convex teams linked to the current Expo app's owner account";
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(IntegrationsConvexTeam, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const connections = await ConvexQuery.getConvexTeamConnectionsByAccountIdAsync(
graphqlClient,
account.id
);
if (connections.length === 0) {
logNoConvexTeams(account.name);
return;
}
Log.log(chalk.bold(`Convex teams linked to @${account.name}`));
for (const [index, connection] of connections.entries()) {
if (index > 0) {
Log.newLine();
}
Log.log(formatConvexTeamConnection(connection));
}
}
}
@@ -1,138 +0,0 @@
import { Args, Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../../../commandUtils/EasCommand';
import {
formatConvexTeam,
formatConvexTeamConnection,
getConvexTeamDashboardUrl,
logNoConvexTeams,
} from '../../../../commandUtils/convex';
import { EASNonInteractiveFlag } from '../../../../commandUtils/flags';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../../graphql/types/ConvexTeamConnection';
import Log, { link } from '../../../../log';
import { ora } from '../../../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../../../prompts';
export default class IntegrationsConvexTeamDelete extends EasCommand {
static override description =
"remove a Convex team link from the current Expo app owner account's EAS servers";
static override args = {
CONVEX_TEAM: Args.string({
required: false,
description: 'Slug of the Convex team to remove',
}),
};
static override flags = {
...EASNonInteractiveFlag,
yes: Flags.boolean({
char: 'y',
description: 'Skip confirmation prompt',
default: false,
}),
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
args: { CONVEX_TEAM: team },
flags: { 'non-interactive': nonInteractive, yes },
} = await this.parse(IntegrationsConvexTeamDelete);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(IntegrationsConvexTeamDelete, {
nonInteractive,
withServerSideEnvironment: null,
});
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const connections = await ConvexQuery.getConvexTeamConnectionsByAccountIdAsync(
graphqlClient,
account.id
);
if (connections.length === 0) {
logNoConvexTeams(account.name);
return;
}
const connection = await this.resolveConnectionAsync(connections, team, nonInteractive);
Log.addNewLineIfNone();
Log.log(formatConvexTeamConnection(connection));
Log.newLine();
const dashboardUrl = getConvexTeamDashboardUrl(connection);
if (!nonInteractive && !yes) {
const confirmed = await confirmAsync({
message: `Remove this Convex team link from EAS servers? This does not destroy resources on Convex. Convex dashboard: ${link(
dashboardUrl,
{ dim: false }
)}`,
});
if (!confirmed) {
Log.error('Canceled deletion of the Convex team link');
return;
}
} else {
Log.warn(
`Removing the Convex team link from EAS servers. This does not destroy resources on Convex: ${dashboardUrl}`
);
}
const spinner = ora('Removing Convex team link').start();
try {
await ConvexMutation.deleteConvexTeamConnectionAsync(graphqlClient, connection.id);
spinner.succeed(
`Removed Convex team ${chalk.bold(formatConvexTeam(connection))} from EAS servers`
);
} catch (error) {
spinner.fail('Failed to remove Convex team link');
throw error;
}
}
private async resolveConnectionAsync(
connections: ConvexTeamConnectionData[],
team: string | undefined,
nonInteractive: boolean
): Promise<ConvexTeamConnectionData> {
if (team) {
const connection = connections.find(
item => item.convexTeamSlug === team || item.convexTeamName === team || item.id === team
);
if (!connection) {
throw new Error(`Convex team ${team} is not linked to this account.`);
}
return connection;
}
if (connections.length === 1) {
return connections[0];
}
if (nonInteractive) {
throw new Error(
'Convex team slug must be provided in non-interactive mode when multiple Convex team links exist.'
);
}
return await selectAsync(
'Select a Convex team link to remove',
connections.map(connection => ({
title: formatConvexTeam(connection),
value: connection,
}))
);
}
}
@@ -1,159 +0,0 @@
import { Args } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../../../commandUtils/EasCommand';
import {
confirmRecentConvexInviteAsync,
formatConvexTeam,
getConvexTeamDashboardUrl,
logNoConvexTeams,
} from '../../../../commandUtils/convex';
import { EASNonInteractiveFlag } from '../../../../commandUtils/flags';
import { ConvexMutation } from '../../../../graphql/mutations/ConvexMutation';
import { ConvexQuery } from '../../../../graphql/queries/ConvexQuery';
import { ConvexTeamConnectionData } from '../../../../graphql/types/ConvexTeamConnection';
import Log, { link } from '../../../../log';
import { ora } from '../../../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../../../project/projectUtils';
import { selectAsync } from '../../../../prompts';
export default class IntegrationsConvexTeamInvite extends EasCommand {
static override description = 'send a Convex team invitation to your verified email address';
static override args = {
CONVEX_TEAM: Args.string({
required: false,
description: 'Slug of the Convex team to invite yourself to',
}),
};
static override flags = {
...EASNonInteractiveFlag,
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
};
async runAsync(): Promise<void> {
const {
args: { CONVEX_TEAM: team },
flags: { 'non-interactive': nonInteractive },
} = await this.parse(IntegrationsConvexTeamInvite);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient, actor },
} = await this.getContextAsync(IntegrationsConvexTeamInvite, {
nonInteractive,
withServerSideEnvironment: null,
});
const email = this.getActorEmail(actor);
if (!email) {
Log.warn(
`Could not determine your verified email address, so no Convex team invitation was sent. Run ${chalk.cyan(
'eas integrations:convex:team:invite'
)} after signing in with a user account.`
);
return;
}
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const connections = await ConvexQuery.getConvexTeamConnectionsByAccountIdAsync(
graphqlClient,
account.id
);
if (connections.length === 0) {
logNoConvexTeams(account.name);
return;
}
const connection = await this.resolveConnectionAsync(connections, team, nonInteractive);
Log.addNewLineIfNone();
this.logTeam(connection);
this.logPreviousInvite(connection);
Log.newLine();
if (!(await confirmRecentConvexInviteAsync(connection, { nonInteractive }))) {
Log.warn('Skipped sending Convex team invitation.');
return;
}
const spinner = ora(`Sending Convex team invitation to ${email}`).start();
try {
await ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync(graphqlClient, {
convexTeamConnectionId: connection.id,
});
spinner.succeed(
`Sent Convex team invitation to ${chalk.bold(email)} for ${chalk.bold(formatConvexTeam(connection))}`
);
Log.log(`Convex dashboard: ${link(getConvexTeamDashboardUrl(connection), { dim: false })}`);
} catch (error) {
spinner.fail('Failed to send Convex team invitation');
throw error;
}
}
private getActorEmail(actor: { __typename: string; [key: string]: any }): string | null {
return 'email' in actor && typeof actor.email === 'string' ? actor.email : null;
}
private logTeam(connection: ConvexTeamConnectionData): void {
Log.log(chalk.bold(`Convex team ${formatConvexTeam(connection)}`));
Log.log(
`${chalk.bold('Dashboard')}: ${link(getConvexTeamDashboardUrl(connection), { dim: false })}`
);
}
private logPreviousInvite(connection: ConvexTeamConnectionData): void {
if (!connection.invitedEmail && !connection.invitedAt) {
return;
}
Log.newLine();
Log.log(chalk.bold('Previous invite'));
if (connection.invitedEmail) {
Log.log(`${chalk.bold('Email')}: ${connection.invitedEmail}`);
}
if (connection.invitedAt) {
Log.log(`${chalk.bold('Sent at')}: ${connection.invitedAt}`);
}
}
private async resolveConnectionAsync(
connections: ConvexTeamConnectionData[],
team: string | undefined,
nonInteractive: boolean
): Promise<ConvexTeamConnectionData> {
if (team) {
const connection = connections.find(
item => item.convexTeamSlug === team || item.convexTeamName === team || item.id === team
);
if (!connection) {
throw new Error(`Convex team ${team} is not linked to this account.`);
}
return connection;
}
if (connections.length === 1) {
return connections[0];
}
if (nonInteractive) {
throw new Error(
'Convex team slug must be provided in non-interactive mode when multiple Convex team links exist.'
);
}
return await selectAsync(
'Select a Convex team link to invite yourself to',
connections.map(connection => ({
title: formatConvexTeam(connection),
value: connection,
}))
);
}
}
-1
View File
@@ -103,7 +103,6 @@ async function createFingerprintWithoutLoggingAsync(
if (options.debug) {
fingerprintOptions.debug = true;
}
fingerprintOptions.silent = true;
return await withTemporaryEnvAsync(options.env ?? {}, () =>
Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions)
+1 -94
View File
@@ -3616,7 +3616,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
releaseChannel?: Maybe<Scalars['String']['output']>;
requiredPackageManager?: Maybe<Scalars['String']['output']>;
resolvedEnvironment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
resolvedImage?: Maybe<Scalars['String']['output']>;
/**
* The builder resource class requested by the developer
* @deprecated Use resourceClassDisplayName instead
@@ -3630,7 +3629,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
/** @deprecated Use 'runtime' field instead. */
runtimeVersion?: Maybe<Scalars['String']['output']>;
sdkVersion?: Maybe<Scalars['String']['output']>;
/** @deprecated Use 'resolvedImage' for the concrete image the build runs on. */
/** @deprecated Check logs instead. */
selectedImage?: Maybe<Scalars['String']['output']>;
status: BuildStatus;
submissions: Array<Submission>;
@@ -4232,32 +4231,12 @@ export type ConvexProject = {
updatedAt: Scalars['DateTime']['output'];
};
export type ConvexProjectMutation = {
__typename?: 'ConvexProjectMutation';
deleteConvexProject: Scalars['ID']['output'];
setupConvexProject: SetupConvexProjectResult;
};
export type ConvexProjectMutationDeleteConvexProjectArgs = {
convexProjectId: Scalars['ID']['input'];
};
export type ConvexProjectMutationSetupConvexProjectArgs = {
input: SetupConvexProjectInput;
};
export type ConvexTeamConnection = {
__typename?: 'ConvexTeamConnection';
account: Account;
convexTeamIdentifier: Scalars['String']['output'];
convexTeamName: Scalars['String']['output'];
convexTeamSlug: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
invitedAt?: Maybe<Scalars['DateTime']['output']>;
invitedEmail?: Maybe<Scalars['String']['output']>;
updatedAt: Scalars['DateTime']['output'];
};
@@ -7857,7 +7836,6 @@ export type RootMutation = {
build: BuildMutation;
/** Mutations that create, update, and delete Build Annotations */
buildAnnotation: BuildAnnotationMutation;
convexProject: ConvexProjectMutation;
convexTeamConnection: ConvexTeamConnectionMutation;
customDomain: CustomDomainMutation;
deployments: DeploymentsMutation;
@@ -8456,21 +8434,6 @@ export type SentryProjectMutationDeleteSentryProjectArgs = {
sentryProjectId: Scalars['ID']['input'];
};
export type SetupConvexProjectInput = {
appId: Scalars['ID']['input'];
convexTeamConnectionId: Scalars['ID']['input'];
deploymentRegion?: InputMaybe<Scalars['String']['input']>;
projectName: Scalars['String']['input'];
};
export type SetupConvexProjectResult = {
__typename?: 'SetupConvexProjectResult';
convexDeploymentName: Scalars['String']['output'];
convexDeploymentUrl: Scalars['String']['output'];
convexProject: ConvexProject;
deployKey: Scalars['String']['output'];
};
export type SizeBreakdownCategory = {
__typename?: 'SizeBreakdownCategory';
assetCount: Scalars['Int']['output'];
@@ -8787,7 +8750,6 @@ export type SubscriptionDetails = {
endedAt?: Maybe<Scalars['DateTime']['output']>;
futureSubscription?: Maybe<FutureSubscription>;
id: Scalars['ID']['output'];
includedAgentCreditsInCents: Scalars['Int']['output'];
isDowngrading?: Maybe<Scalars['Boolean']['output']>;
meteredBillingStatus: MeteredBillingStatus;
name?: Maybe<Scalars['String']['output']>;
@@ -11942,41 +11904,6 @@ export type RetryIosBuildMutationVariables = Exact<{
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'PartnerActor', id: string, displayName: string } | { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type CreateConvexTeamConnectionMutationVariables = Exact<{
convexTeamConnectionData: CreateConvexTeamConnectionInput;
}>;
export type CreateConvexTeamConnectionMutation = { __typename?: 'RootMutation', convexTeamConnection: { __typename?: 'ConvexTeamConnectionMutation', createConvexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } } };
export type DeleteConvexTeamConnectionMutationVariables = Exact<{
convexTeamConnectionId: Scalars['ID']['input'];
}>;
export type DeleteConvexTeamConnectionMutation = { __typename?: 'RootMutation', convexTeamConnection: { __typename?: 'ConvexTeamConnectionMutation', deleteConvexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } } };
export type SetupConvexProjectMutationVariables = Exact<{
input: SetupConvexProjectInput;
}>;
export type SetupConvexProjectMutation = { __typename?: 'RootMutation', convexProject: { __typename?: 'ConvexProjectMutation', setupConvexProject: { __typename?: 'SetupConvexProjectResult', convexDeploymentName: string, convexDeploymentUrl: string, deployKey: string, convexProject: { __typename?: 'ConvexProject', id: string, convexProjectIdentifier: string, convexProjectName: string, convexProjectSlug: string, createdAt: any, updatedAt: any, convexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } } } } };
export type DeleteConvexProjectMutationVariables = Exact<{
convexProjectId: Scalars['ID']['input'];
}>;
export type DeleteConvexProjectMutation = { __typename?: 'RootMutation', convexProject: { __typename?: 'ConvexProjectMutation', deleteConvexProject: string } };
export type SendConvexTeamInviteToVerifiedEmailMutationVariables = Exact<{
input: SendConvexTeamInviteToVerifiedEmailInput;
}>;
export type SendConvexTeamInviteToVerifiedEmailMutation = { __typename?: 'RootMutation', convexTeamConnection: { __typename?: 'ConvexTeamConnectionMutation', sendConvexTeamInviteToVerifiedEmail: boolean } };
export type CreateDeviceRunSessionMutationVariables = Exact<{
deviceRunSessionInput: CreateDeviceRunSessionInput;
}>;
@@ -12458,20 +12385,6 @@ export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
export type ViewUpdateChannelsPaginatedOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, channelsPaginated: { __typename?: 'AppChannelsConnection', edges: Array<{ __typename?: 'AppChannelEdge', cursor: string, node: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } } };
export type ConvexTeamConnectionsByAccountIdQueryVariables = Exact<{
accountId: Scalars['String']['input'];
}>;
export type ConvexTeamConnectionsByAccountIdQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, convexTeamConnections: Array<{ __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null }> } } };
export type ConvexProjectByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
export type ConvexProjectByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, convexProject?: { __typename?: 'ConvexProject', id: string, convexProjectIdentifier: string, convexProjectName: string, convexProjectSlug: string, createdAt: any, updatedAt: any, convexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } } | null } } };
export type DeviceRunSessionByIdQueryVariables = Exact<{
deviceRunSessionId: Scalars['ID']['input'];
}>;
@@ -12759,12 +12672,6 @@ export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, s
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'PartnerActor', id: string, displayName: string } | { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type ConvexTeamConnectionFragment = { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null };
export type ConvexProjectFragment = { __typename?: 'ConvexProject', id: string, convexProjectIdentifier: string, convexProjectName: string, convexProjectSlug: string, createdAt: any, updatedAt: any, convexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } };
export type SetupConvexProjectResultFragment = { __typename?: 'SetupConvexProjectResult', convexDeploymentName: string, convexDeploymentUrl: string, deployKey: string, convexProject: { __typename?: 'ConvexProject', id: string, convexProjectIdentifier: string, convexProjectName: string, convexProjectSlug: string, createdAt: any, updatedAt: any, convexTeamConnection: { __typename?: 'ConvexTeamConnection', id: string, convexTeamIdentifier: string, convexTeamName: string, convexTeamSlug: string, createdAt: any, updatedAt: any, invitedAt?: any | null, invitedEmail?: string | null } } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
@@ -1,196 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
CreateConvexTeamConnectionInput,
SendConvexTeamInviteToVerifiedEmailInput,
SetupConvexProjectInput,
} from '../generated';
import {
ConvexProjectFragmentNode,
ConvexTeamConnectionData,
ConvexTeamConnectionFragmentNode,
SetupConvexProjectResultData,
SetupConvexProjectResultFragmentNode,
} from '../types/ConvexTeamConnection';
type CreateConvexTeamConnectionMutation = {
convexTeamConnection: {
createConvexTeamConnection: ConvexTeamConnectionData;
};
};
type CreateConvexTeamConnectionMutationVariables = {
convexTeamConnectionData: CreateConvexTeamConnectionInput;
};
type DeleteConvexTeamConnectionMutation = {
convexTeamConnection: {
deleteConvexTeamConnection: ConvexTeamConnectionData;
};
};
type DeleteConvexTeamConnectionMutationVariables = {
convexTeamConnectionId: string;
};
type SetupConvexProjectMutation = {
convexProject: {
setupConvexProject: SetupConvexProjectResultData;
};
};
type SetupConvexProjectMutationVariables = {
input: SetupConvexProjectInput;
};
type DeleteConvexProjectMutation = {
convexProject: {
deleteConvexProject: string;
};
};
type DeleteConvexProjectMutationVariables = {
convexProjectId: string;
};
type SendConvexTeamInviteToVerifiedEmailMutation = {
convexTeamConnection: {
sendConvexTeamInviteToVerifiedEmail: boolean;
};
};
type SendConvexTeamInviteToVerifiedEmailMutationVariables = {
input: SendConvexTeamInviteToVerifiedEmailInput;
};
export const ConvexMutation = {
async createConvexTeamConnectionAsync(
graphqlClient: ExpoGraphqlClient,
input: CreateConvexTeamConnectionInput
): Promise<ConvexTeamConnectionData> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateConvexTeamConnectionMutation, CreateConvexTeamConnectionMutationVariables>(
gql`
mutation CreateConvexTeamConnection(
$convexTeamConnectionData: CreateConvexTeamConnectionInput!
) {
convexTeamConnection {
createConvexTeamConnection(
convexTeamConnectionData: $convexTeamConnectionData
) {
id
...ConvexTeamConnectionFragment
}
}
}
${print(ConvexTeamConnectionFragmentNode)}
`,
{ convexTeamConnectionData: input }
)
.toPromise()
);
return data.convexTeamConnection.createConvexTeamConnection;
},
async deleteConvexTeamConnectionAsync(
graphqlClient: ExpoGraphqlClient,
convexTeamConnectionId: string
): Promise<ConvexTeamConnectionData> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteConvexTeamConnectionMutation, DeleteConvexTeamConnectionMutationVariables>(
gql`
mutation DeleteConvexTeamConnection($convexTeamConnectionId: ID!) {
convexTeamConnection {
deleteConvexTeamConnection(
convexTeamConnectionId: $convexTeamConnectionId
) {
id
...ConvexTeamConnectionFragment
}
}
}
${print(ConvexTeamConnectionFragmentNode)}
`,
{ convexTeamConnectionId }
)
.toPromise()
);
return data.convexTeamConnection.deleteConvexTeamConnection;
},
async setupConvexProjectAsync(
graphqlClient: ExpoGraphqlClient,
input: SetupConvexProjectInput
): Promise<SetupConvexProjectResultData> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetupConvexProjectMutation, SetupConvexProjectMutationVariables>(
gql`
mutation SetupConvexProject($input: SetupConvexProjectInput!) {
convexProject {
setupConvexProject(input: $input) {
...SetupConvexProjectResultFragment
}
}
}
${print(ConvexTeamConnectionFragmentNode)}
${print(ConvexProjectFragmentNode)}
${print(SetupConvexProjectResultFragmentNode)}
`,
{ input }
)
.toPromise()
);
return data.convexProject.setupConvexProject;
},
async deleteConvexProjectAsync(
graphqlClient: ExpoGraphqlClient,
convexProjectId: string
): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteConvexProjectMutation, DeleteConvexProjectMutationVariables>(
gql`
mutation DeleteConvexProject($convexProjectId: ID!) {
convexProject {
deleteConvexProject(convexProjectId: $convexProjectId)
}
}
`,
{ convexProjectId }
)
.toPromise()
);
return data.convexProject.deleteConvexProject;
},
async sendConvexTeamInviteToVerifiedEmailAsync(
graphqlClient: ExpoGraphqlClient,
input: SendConvexTeamInviteToVerifiedEmailInput
): Promise<boolean> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
SendConvexTeamInviteToVerifiedEmailMutation,
SendConvexTeamInviteToVerifiedEmailMutationVariables
>(
gql`
mutation SendConvexTeamInviteToVerifiedEmail($input: SendConvexTeamInviteToVerifiedEmailInput!) {
convexTeamConnection {
sendConvexTeamInviteToVerifiedEmail(input: $input)
}
}
`,
{ input }
)
.toPromise()
);
return data.convexTeamConnection.sendConvexTeamInviteToVerifiedEmail;
},
};
@@ -1,102 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
ConvexProjectData,
ConvexProjectFragmentNode,
ConvexTeamConnectionData,
ConvexTeamConnectionFragmentNode,
} from '../types/ConvexTeamConnection';
type ConvexTeamConnectionsByAccountIdQuery = {
account: {
byId: {
id: string;
convexTeamConnections: ConvexTeamConnectionData[];
};
};
};
type ConvexTeamConnectionsByAccountIdQueryVariables = {
accountId: string;
};
type ConvexProjectByAppIdQuery = {
app: {
byId: {
id: string;
convexProject?: ConvexProjectData | null;
};
};
};
type ConvexProjectByAppIdQueryVariables = {
appId: string;
};
export const ConvexQuery = {
async getConvexTeamConnectionsByAccountIdAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string
): Promise<ConvexTeamConnectionData[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<
ConvexTeamConnectionsByAccountIdQuery,
ConvexTeamConnectionsByAccountIdQueryVariables
>(
gql`
query ConvexTeamConnectionsByAccountId($accountId: String!) {
account {
byId(accountId: $accountId) {
id
convexTeamConnections {
id
...ConvexTeamConnectionFragment
}
}
}
}
${print(ConvexTeamConnectionFragmentNode)}
`,
{ accountId }
)
.toPromise()
);
return data.account.byId.convexTeamConnections;
},
async getConvexProjectByAppIdAsync(
graphqlClient: ExpoGraphqlClient,
appId: string
): Promise<ConvexProjectData | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<ConvexProjectByAppIdQuery, ConvexProjectByAppIdQueryVariables>(
gql`
query ConvexProjectByAppId($appId: String!) {
app {
byId(appId: $appId) {
id
convexProject {
id
...ConvexProjectFragment
}
}
}
}
${print(ConvexTeamConnectionFragmentNode)}
${print(ConvexProjectFragmentNode)}
`,
{ appId },
{ additionalTypenames: ['App', 'ConvexProject'] }
)
.toPromise()
);
return data.app.byId.convexProject ?? null;
},
};
@@ -1,74 +0,0 @@
import gql from 'graphql-tag';
import { ConvexProject, ConvexTeamConnection, SetupConvexProjectResult } from '../generated';
export type ConvexTeamConnectionData = Pick<
ConvexTeamConnection,
| 'id'
| 'convexTeamIdentifier'
| 'convexTeamName'
| 'convexTeamSlug'
| 'createdAt'
| 'updatedAt'
| 'invitedAt'
| 'invitedEmail'
>;
export type ConvexProjectData = Pick<
ConvexProject,
| 'id'
| 'convexProjectIdentifier'
| 'convexProjectName'
| 'convexProjectSlug'
| 'createdAt'
| 'updatedAt'
> & {
convexTeamConnection: ConvexTeamConnectionData;
};
export type SetupConvexProjectResultData = Pick<
SetupConvexProjectResult,
'convexDeploymentName' | 'convexDeploymentUrl' | 'deployKey'
> & {
convexProject: ConvexProjectData;
};
export const ConvexTeamConnectionFragmentNode = gql`
fragment ConvexTeamConnectionFragment on ConvexTeamConnection {
id
convexTeamIdentifier
convexTeamName
convexTeamSlug
createdAt
updatedAt
invitedAt
invitedEmail
}
`;
export const ConvexProjectFragmentNode = gql`
fragment ConvexProjectFragment on ConvexProject {
id
convexProjectIdentifier
convexProjectName
convexProjectSlug
createdAt
updatedAt
convexTeamConnection {
id
...ConvexTeamConnectionFragment
}
}
`;
export const SetupConvexProjectResultFragmentNode = gql`
fragment SetupConvexProjectResultFragment on SetupConvexProjectResult {
convexDeploymentName
convexDeploymentUrl
deployKey
convexProject {
id
...ConvexProjectFragment
}
}
`;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "eas-cli-local-build-plugin",
"version": "18.10.0",
"version": "18.9.0",
"description": "Tool for running EAS compatible builds on a local machine.",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "BUSL-1.1",
@@ -26,7 +26,7 @@
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
},
"dependencies": {
"@expo/build-tools": "18.10.0",
"@expo/build-tools": "18.9.0",
"@expo/eas-build-job": "18.8.0",
"@expo/logger": "18.5.0",
"@expo/spawn-async": "^1.7.2",
+13
View File
@@ -0,0 +1,13 @@
import type { Config } from '@jest/types';
import baseConfig from '../../jest/jest.shared.config';
const config: Config.InitialOptions = {
...baseConfig,
rootDir: 'src',
testMatch: ['**/__tests__/*.test.ts'],
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/../jest/setup-tests.ts'],
};
export default config;
@@ -0,0 +1,5 @@
if (process.env.NODE_ENV !== 'test') {
throw new Error("NODE_ENV environment variable must be set to 'test'.");
}
// Always mock:
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@expo/lockfile-utils",
"version": "18.0.1",
"description": "Parse npm, yarn, pnpm, and bun lockfiles and validate them against package.json for version mismatches and peer dependency conflicts",
"bugs": "https://github.com/expo/eas-cli/issues",
"license": "MIT",
"author": "Expo <[email protected]>",
"repository": {
"type": "git",
"url": "https://github.com/expo/eas-cli.git",
"directory": "packages/lockfile-utils"
},
"files": [
"dist"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"start": "yarn watch",
"watch": "tsc --watch --preserveWatchOutput",
"build": "tsc",
"typecheck": "tsc",
"prepack": "rm -rf dist *.tsbuildinfo && tsc -p tsconfig.build.json",
"test": "jest --config jest.config.ts",
"test:watch": "jest --config jest.config.ts --watch",
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
},
"dependencies": {
"semver": "^7.6.0"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "20.14.2",
"@types/semver": "^7.5.8",
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"typescript": "^5.5.4"
}
}
@@ -0,0 +1,152 @@
import {
parseLockfile,
parseNpmLock,
parseYarnLock,
parsePnpmLock,
parseBunLock,
} from '../parsers';
const NPM_LOCKFILE = JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.4' },
'node_modules/react-native': { version: '0.79.4' },
'node_modules/react/node_modules/loose-envify': { version: '1.4.0' },
},
});
const NPM_LOCKFILE_WITH_PEERS = JSON.stringify({
lockfileVersion: 3,
packages: {
'node_modules/react': { version: '19.0.0' },
'node_modules/react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
'node_modules/@0no-co/graphql.web': {
version: '1.2.0',
peerDependencies: { graphql: '^14.0.0 || ^15.0.0 || ^16.0.0' },
peerDependenciesMeta: { graphql: { optional: true } },
},
},
});
const YARN_LOCKFILE = `# yarn lockfile v1
[email protected]:
version "19.0.4"
resolved "https://registry.yarnpkg.com/react/-/react-19.0.4.tgz"
react-native@~0.79.0:
version "0.79.4"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.4.tgz"
`;
const BUN_LOCKFILE = `{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"react": "19.0.4",
},
},
},
"packages": {
"react": ["[email protected]", "", {}, "sha512-abc"],
"react-dom": ["[email protected]", "", { "peerDependencies": { "react": "^19.2.4" } }, "sha512-def"],
"@0no-co/graphql.web": ["@0no-co/[email protected]", "", { "peerDependencies": { "graphql": "^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-ghi"],
},
}`;
const PNPM_LOCKFILE = `lockfileVersion: '9.0'
packages:
[email protected]:
resolution: {integrity: sha512-abc}
@types/[email protected]:
resolution: {integrity: sha512-def}
`;
describe('parseNpmLock', () => {
it('parses top-level packages', () => {
const result = parseNpmLock(NPM_LOCKFILE);
expect(result).not.toBeNull();
expect(result!.packages.get('react')?.version).toBe('19.0.4');
expect(result!.packages.get('react-native')?.version).toBe('0.79.4');
});
it('skips nested node_modules (transitive deps)', () => {
const result = parseNpmLock(NPM_LOCKFILE);
expect(result!.packages.has('loose-envify')).toBe(false);
});
it('parses peer dependencies and optional peers', () => {
const result = parseNpmLock(NPM_LOCKFILE_WITH_PEERS);
const reactDom = result!.packages.get('react-dom');
expect(reactDom?.peerDependencies).toEqual({ react: '^19.2.4' });
expect(reactDom?.optionalPeers).toBeUndefined();
const graphqlWeb = result!.packages.get('@0no-co/graphql.web');
expect(graphqlWeb?.optionalPeers?.has('graphql')).toBe(true);
});
it('returns null for invalid JSON', () => {
expect(parseNpmLock('not json')).toBeNull();
});
});
describe('parseYarnLock', () => {
it('parses package versions', () => {
const result = parseYarnLock(YARN_LOCKFILE);
expect(result).not.toBeNull();
expect(result!.packages.get('react')?.version).toBe('19.0.4');
expect(result!.packages.get('react-native')?.version).toBe('0.79.4');
});
});
describe('parsePnpmLock', () => {
it('parses package versions including scoped packages', () => {
const result = parsePnpmLock(PNPM_LOCKFILE);
expect(result).not.toBeNull();
expect(result!.packages.get('react')?.version).toBe('19.0.4');
expect(result!.packages.get('@types/react')?.version).toBe('19.0.4');
});
});
describe('parseBunLock', () => {
it('parses package versions from JSONC format', () => {
const result = parseBunLock(BUN_LOCKFILE);
expect(result).not.toBeNull();
expect(result!.packages.get('react')?.version).toBe('19.0.4');
});
it('parses peer dependencies and optional peers', () => {
const result = parseBunLock(BUN_LOCKFILE);
const reactDom = result!.packages.get('react-dom');
expect(reactDom?.peerDependencies).toEqual({ react: '^19.2.4' });
const graphqlWeb = result!.packages.get('@0no-co/graphql.web');
expect(graphqlWeb?.optionalPeers?.has('graphql')).toBe(true);
});
it('returns null for invalid content', () => {
expect(parseBunLock('not json at all {')).toBeNull();
});
});
describe('parseLockfile', () => {
it('dispatches to the correct parser by manager', () => {
expect(parseLockfile(NPM_LOCKFILE, 'npm', 'package-lock.json')).not.toBeNull();
expect(parseLockfile(YARN_LOCKFILE, 'yarn', 'yarn.lock')).not.toBeNull();
expect(parseLockfile(PNPM_LOCKFILE, 'pnpm', 'pnpm-lock.yaml')).not.toBeNull();
expect(parseLockfile(BUN_LOCKFILE, 'bun', 'bun.lock')).not.toBeNull();
});
it('returns null for binary bun.lockb', () => {
expect(parseLockfile('binary', 'bun', 'bun.lockb')).toBeNull();
});
it('returns null for unknown manager', () => {
expect(parseLockfile('{}', 'deno', 'deno.lock')).toBeNull();
});
});
@@ -0,0 +1,118 @@
import { findLockfileMismatches, findPeerDependencyConflicts } from '../validators';
import { LockfileData, PackageInfo } from '../types';
function makeLockfileData(packages: Record<string, PackageInfo>): LockfileData {
return { packages: new Map(Object.entries(packages)) };
}
describe('findLockfileMismatches', () => {
it('returns empty when lockfile is in sync', () => {
const packageJson = { dependencies: { react: '19.0.4', 'react-native': '~0.79.0' } };
const lockfile = makeLockfileData({
react: { version: '19.0.4' },
'react-native': { version: '0.79.4' },
});
expect(findLockfileMismatches(packageJson, lockfile)).toEqual([]);
});
it('detects version mismatch', () => {
const packageJson = { dependencies: { react: '19.0.4' } };
const lockfile = makeLockfileData({ react: { version: '19.0.0' } });
const mismatches = findLockfileMismatches(packageJson, lockfile);
expect(mismatches).toHaveLength(1);
expect(mismatches[0]).toEqual({
name: 'react',
specifier: '19.0.4',
lockedVersion: '19.0.0',
});
});
it('checks devDependencies and optionalDependencies', () => {
const packageJson = {
devDependencies: { typescript: '^5.5.0' },
optionalDependencies: { fsevents: '^2.3.0' },
};
const lockfile = makeLockfileData({
typescript: { version: '5.5.4' },
fsevents: { version: '2.3.3' },
});
expect(findLockfileMismatches(packageJson, lockfile)).toEqual([]);
});
it('skips non-registry specifiers', () => {
const packageJson = {
dependencies: {
'my-local': 'file:../local',
'my-workspace': 'workspace:*',
'my-git': 'git+https://github.com/foo/bar.git',
'my-github': 'github:foo/bar',
'my-latest': 'latest',
},
};
const lockfile = makeLockfileData({});
expect(findLockfileMismatches(packageJson, lockfile)).toEqual([]);
});
it('skips packages not found in lockfile', () => {
const packageJson = { dependencies: { 'new-dep': '^1.0.0' } };
const lockfile = makeLockfileData({});
expect(findLockfileMismatches(packageJson, lockfile)).toEqual([]);
});
});
describe('findPeerDependencyConflicts', () => {
it('returns empty when peers are satisfied', () => {
const packageJson = { dependencies: { react: '19.2.4', 'react-dom': '19.2.4' } };
const lockfile = makeLockfileData({
react: { version: '19.2.4' },
'react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
});
expect(findPeerDependencyConflicts(packageJson, lockfile)).toEqual([]);
});
it('detects unsatisfied peer dependency', () => {
const packageJson = { dependencies: { react: '19.0.0', 'react-dom': '19.2.4' } };
const lockfile = makeLockfileData({
react: { version: '19.0.0' },
'react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
});
const conflicts = findPeerDependencyConflicts(packageJson, lockfile);
expect(conflicts).toHaveLength(1);
expect(conflicts[0]).toEqual({
source: 'react-dom',
sourceVersion: '19.2.4',
peer: 'react',
peerRange: '^19.2.4',
installedVersion: '19.0.0',
});
});
it('skips optional peers', () => {
const packageJson = { dependencies: { '@0no-co/graphql.web': '1.2.0' } };
const lockfile = makeLockfileData({
'@0no-co/graphql.web': {
version: '1.2.0',
peerDependencies: { graphql: '^14.0.0 || ^15.0.0 || ^16.0.0' },
optionalPeers: new Set(['graphql']),
},
});
expect(findPeerDependencyConflicts(packageJson, lockfile)).toEqual([]);
});
it('skips peers not installed in lockfile', () => {
const packageJson = { dependencies: { 'react-dom': '19.2.4' } };
const lockfile = makeLockfileData({
'react-dom': {
version: '19.2.4',
peerDependencies: { react: '^19.2.4' },
},
});
expect(findPeerDependencyConflicts(packageJson, lockfile)).toEqual([]);
});
});
+8
View File
@@ -0,0 +1,8 @@
export { parseLockfile, parseNpmLock, parseYarnLock, parsePnpmLock, parseBunLock } from './parsers';
export { findLockfileMismatches, findPeerDependencyConflicts } from './validators';
export type {
PackageInfo,
LockfileData,
DependencyMismatch,
PeerDependencyConflict,
} from './types';
+154
View File
@@ -0,0 +1,154 @@
import { LockfileData, PackageInfo } from './types';
export function parseNpmLock(content: string): LockfileData | null {
try {
const lockfile = JSON.parse(content);
const rawPackages: Record<
string,
{
version?: string;
peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
}
> = lockfile.packages ?? {};
const packages = new Map<string, PackageInfo>();
for (const [key, value] of Object.entries(rawPackages)) {
if (!key.startsWith('node_modules/')) {
continue;
}
const rest = key.slice('node_modules/'.length);
// Skip nested node_modules (transitive deps)
if (rest.includes('node_modules/')) {
continue;
}
if (value.version) {
const optionalPeers = new Set<string>();
if (value.peerDependenciesMeta) {
for (const [peer, meta] of Object.entries(value.peerDependenciesMeta)) {
if (meta.optional) {
optionalPeers.add(peer);
}
}
}
packages.set(rest, {
version: value.version,
peerDependencies: value.peerDependencies,
optionalPeers: optionalPeers.size > 0 ? optionalPeers : undefined,
});
}
}
return { packages };
} catch {
return null;
}
}
export function parseYarnLock(content: string): LockfileData | null {
try {
const packages = new Map<string, PackageInfo>();
const lines = content.split('\n');
let currentPackage: string | null = null;
for (const line of lines) {
// Entry header: not indented, not a comment, ends with ':'
if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('#') && line.endsWith(':')) {
// Examples:
// yarn v1: react@^19.0.0: or "react@^19.0.0":
// yarn v2+: "react@npm:^19.0.0":
const entry = line.replace(/^"/, '').replace(/:$/, '').replace(/"$/, '');
const atIndex = entry.indexOf('@', entry.startsWith('@') ? 1 : 0);
if (atIndex > 0) {
currentPackage = entry.slice(0, atIndex);
}
}
// Version line (indented): ` version "19.0.0"` or ` version: 19.0.0`
if (currentPackage) {
const versionMatch = line.match(/^\s+version:?\s+"?([^"\s]+)"?/);
if (versionMatch) {
if (!packages.has(currentPackage)) {
packages.set(currentPackage, { version: versionMatch[1] });
}
currentPackage = null;
}
}
}
// yarn.lock doesn't include peer dependency info
return { packages };
} catch {
return null;
}
}
export function parsePnpmLock(content: string): LockfileData | null {
try {
const packages = new Map<string, PackageInfo>();
// Match entries in the packages section like:
// [email protected]: or @types/[email protected]:
const packageRegex = /^ {2}((?:@[\w.-]+\/)?[\w.-]+)@(\d+[^:\s]*?):/gm;
let match;
while ((match = packageRegex.exec(content)) !== null) {
if (!packages.has(match[1])) {
packages.set(match[1], { version: match[2] });
}
}
// pnpm-lock.yaml doesn't readily expose peer deps in a parseable way without YAML
return { packages };
} catch {
return null;
}
}
export function parseBunLock(content: string): LockfileData | null {
try {
// bun.lock is JSONC (has trailing commas) — strip them for JSON.parse
const jsonContent = content.replace(/,(\s*[}\]])/g, '$1');
const lockfile = JSON.parse(jsonContent);
const rawPackages: Record<string, unknown[]> = lockfile.packages ?? {};
const packages = new Map<string, PackageInfo>();
for (const [name, value] of Object.entries(rawPackages)) {
// Format: "react": ["[email protected]", "", { peerDependencies: {...}, optionalPeers: [...] }, "sha512-..."]
if (Array.isArray(value) && typeof value[0] === 'string') {
const entry: string = value[0];
const atIndex = entry.lastIndexOf('@');
if (atIndex > 0) {
const meta = (typeof value[2] === 'object' && value[2] !== null ? value[2] : {}) as {
peerDependencies?: Record<string, string>;
optionalPeers?: string[];
};
packages.set(name, {
version: entry.slice(atIndex + 1),
peerDependencies: meta.peerDependencies,
optionalPeers: meta.optionalPeers ? new Set(meta.optionalPeers) : undefined,
});
}
}
}
return { packages };
} catch {
return null;
}
}
export function parseLockfile(
content: string,
manager: string,
lockfilePath: string
): LockfileData | null {
switch (manager) {
case 'npm':
return parseNpmLock(content);
case 'yarn':
return parseYarnLock(content);
case 'pnpm':
return parsePnpmLock(content);
case 'bun':
// bun.lockb is binary — can only parse the text variant
if (lockfilePath.endsWith('.lockb')) {
return null;
}
return parseBunLock(content);
default:
return null;
}
}
+26
View File
@@ -0,0 +1,26 @@
export interface PackageInfo {
version: string;
peerDependencies?: Record<string, string>;
optionalPeers?: Set<string>;
}
export interface LockfileData {
packages: Map<string, PackageInfo>;
}
export interface DependencyMismatch {
name: string;
specifier: string;
lockedVersion: string;
}
export interface PeerDependencyConflict {
/** Package that declares the peer dependency */
source: string;
sourceVersion: string;
/** The peer dependency that is not satisfied */
peer: string;
peerRange: string;
/** The actual version of the peer in the lockfile */
installedVersion: string;
}
+92
View File
@@ -0,0 +1,92 @@
import semver from 'semver';
import { DependencyMismatch, LockfileData, PeerDependencyConflict } from './types';
function isNonRegistrySpecifier(specifier: string): boolean {
return (
specifier.startsWith('file:') ||
specifier.startsWith('link:') ||
specifier.startsWith('workspace:') ||
specifier.startsWith('git:') ||
specifier.startsWith('git+') ||
specifier.startsWith('github:') ||
specifier.startsWith('http:') ||
specifier.startsWith('https:') ||
specifier.startsWith('portal:') ||
specifier === '*' ||
specifier === 'latest' ||
specifier === 'next'
);
}
export function findLockfileMismatches(
packageJson: Record<string, unknown>,
lockfileData: LockfileData
): DependencyMismatch[] {
const allDeps: Record<string, string> = {
...(packageJson.dependencies as Record<string, string> | undefined),
...(packageJson.devDependencies as Record<string, string> | undefined),
...(packageJson.optionalDependencies as Record<string, string> | undefined),
};
const mismatches: DependencyMismatch[] = [];
for (const [name, specifier] of Object.entries(allDeps)) {
if (isNonRegistrySpecifier(specifier)) {
continue;
}
const pkg = lockfileData.packages.get(name);
if (!pkg) {
continue; // Package not found in lockfile — may be a new dep or hoisted differently
}
if (!semver.satisfies(pkg.version, specifier)) {
mismatches.push({ name, specifier, lockedVersion: pkg.version });
}
}
return mismatches;
}
export function findPeerDependencyConflicts(
packageJson: Record<string, unknown>,
lockfileData: LockfileData
): PeerDependencyConflict[] {
// Check peer deps of direct dependencies only (most common source of build failures)
const directDeps: Record<string, string> = {
...(packageJson.dependencies as Record<string, string> | undefined),
...(packageJson.devDependencies as Record<string, string> | undefined),
};
const conflicts: PeerDependencyConflict[] = [];
for (const depName of Object.keys(directDeps)) {
const pkg = lockfileData.packages.get(depName);
if (!pkg?.peerDependencies) {
continue;
}
for (const [peer, peerRange] of Object.entries(pkg.peerDependencies)) {
// Skip optional peers
if (pkg.optionalPeers?.has(peer)) {
continue;
}
const peerPkg = lockfileData.packages.get(peer);
if (!peerPkg) {
continue; // Peer not installed — might be optional or provided by a parent
}
if (!semver.satisfies(peerPkg.version, peerRange)) {
conflicts.push({
source: depName,
sourceVersion: pkg.version,
peer,
peerRange,
installedVersion: peerPkg.version,
});
}
}
}
return conflicts;
}
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["src/**/__mocks__/**/*.ts", "src/**/__tests__/**/*.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"composite": true,
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*"]
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/worker",
"version": "18.10.0",
"version": "18.9.0",
"private": true,
"scripts": {
"start": "yarn watch",
@@ -19,7 +19,7 @@
},
"dependencies": {
"@babel/core": "7.24.7",
"@expo/build-tools": "18.10.0",
"@expo/build-tools": "18.9.0",
"@expo/config-plugins": "55.0.7",
"@expo/config-types": "55.0.5",
"@expo/downloader": "18.5.0",
@@ -65,11 +65,6 @@ jest.mock('@expo/build-tools', () => {
})),
};
});
jest.mock('@expo/turtle-spawn', () =>
jest.fn(async () => {
throw new Error('Cannot find module expo/package.json');
})
);
jest.mock('../service', () => {
return function () {
const BuildService = new (jest.requireActual('../service').default)();
@@ -85,9 +80,6 @@ jest.mock('../config', () => {
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
};
});
jest.mock('../utils/turtleFetch', () => ({
turtleFetch: jest.fn(async () => ({ ok: true })),
}));
jest.mock('../upload', () => {
return {
...jest.requireActual('../upload'),
Generated
+19 -3
View File
@@ -2856,7 +2856,7 @@ __metadata:
languageName: node
linkType: hard
"@expo/build-tools@npm:18.10.0, @expo/build-tools@workspace:packages/build-tools":
"@expo/build-tools@npm:18.9.0, @expo/build-tools@workspace:packages/build-tools":
version: 0.0.0-use.local
resolution: "@expo/build-tools@workspace:packages/build-tools"
dependencies:
@@ -3270,6 +3270,21 @@ __metadata:
languageName: node
linkType: hard
"@expo/lockfile-utils@npm:18.0.1, @expo/lockfile-utils@workspace:packages/lockfile-utils":
version: 0.0.0-use.local
resolution: "@expo/lockfile-utils@workspace:packages/lockfile-utils"
dependencies:
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:20.14.2"
"@types/semver": "npm:^7.5.8"
jest: "npm:^29.7.0"
semver: "npm:^7.6.0"
ts-jest: "npm:^29.1.4"
typescript: "npm:^5.5.4"
languageName: unknown
linkType: soft
"@expo/logger@npm:18.0.1, @expo/logger@workspace:packages/logger":
"@expo/logger@npm:18.5.0, @expo/logger@workspace:packages/logger":
version: 0.0.0-use.local
resolution: "@expo/logger@workspace:packages/logger"
@@ -3554,7 +3569,7 @@ __metadata:
resolution: "@expo/worker@workspace:packages/worker"
dependencies:
"@babel/core": "npm:7.24.7"
"@expo/build-tools": "npm:18.10.0"
"@expo/build-tools": "npm:18.9.0"
"@expo/config-plugins": "npm:55.0.7"
"@expo/config-types": "npm:55.0.5"
"@expo/downloader": "npm:18.5.0"
@@ -10944,7 +10959,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
dependencies:
"@expo/build-tools": "npm:18.10.0"
"@expo/build-tools": "npm:18.9.0"
"@expo/eas-build-job": "npm:18.8.0"
"@expo/logger": "npm:18.5.0"
"@expo/spawn-async": "npm:^1.7.2"
@@ -11006,6 +11021,7 @@ __metadata:
"@expo/eas-json": "npm:18.8.0"
"@expo/env": "npm:^1.0.0"
"@expo/json-file": "npm:8.3.3"
"@expo/lockfile-utils": "npm:18.0.1"
"@expo/logger": "npm:18.5.0"
"@expo/multipart-body-parser": "npm:2.0.0"
"@expo/osascript": "npm:2.1.4"