@e2e-dev/eas 0.3.2-nightly-20261015074742 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.build.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.3.2-nightly-20261015074742"}
1
+ {"version":"0.3.2"}
package/dist/client.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * EAS Simulators' sessions through Expo's GraphQL API over `fetch`, the API
3
3
  * `eas simulator:*` uses, so the integration installs no SDK.
4
4
  */
5
+ import { isRecord } from './json.js';
5
6
  const GRAPHQL_URL = 'https://api.expo.dev/graphql';
6
7
  const CREATE = `mutation ($input: CreateDeviceRunSessionInput!) {
7
8
  deviceRunSession { createDeviceRunSession(deviceRunSessionInput: $input) { id app { slug ownerAccount { name } } } }
@@ -26,7 +27,7 @@ const ENDED_JOB = new Set(['ERRORED', 'CANCELED', 'FINISHED', 'PENDING_CANCEL'])
26
27
  /** EAS Simulators sessions for one Expo access token or eas-cli session. */
27
28
  export function easSessions(credentials) {
28
29
  const auth = 'accessToken' in credentials ? { Authorization: `Bearer ${credentials.accessToken}` } : { 'expo-session': credentials.sessionSecret };
29
- /** Runs one operation; a GraphQL error throws with EAS's message and error code. */
30
+ /** Runs one operation and returns its unchecked `data`; a GraphQL error throws with EAS's message and error code. */
30
31
  const graphql = async (query, variables, signal) => {
31
32
  const response = await fetch(GRAPHQL_URL, {
32
33
  method: 'POST',
@@ -42,12 +43,17 @@ export function easSessions(credentials) {
42
43
  catch {
43
44
  throw new Error(`EAS: HTTP ${response.status}, ${text.trim().slice(0, 200)}`);
44
45
  }
45
- const error = body.errors?.[0];
46
- if (error !== undefined)
47
- throw new EasError(error.message ?? `HTTP ${response.status}`, error.extensions?.errorCode);
48
- if (!response.ok || body.data == null)
46
+ const errors = field(body, 'errors');
47
+ const error = Array.isArray(errors) ? errors[0] : undefined;
48
+ if (error !== undefined) {
49
+ const message = field(error, 'message');
50
+ const code = field(field(error, 'extensions'), 'errorCode');
51
+ throw new EasError(typeof message === 'string' ? message : `HTTP ${response.status}`, typeof code === 'string' ? code : undefined);
52
+ }
53
+ const data = field(body, 'data');
54
+ if (!response.ok || data == null)
49
55
  throw new Error(`EAS: HTTP ${response.status}, no data`);
50
- return body.data;
56
+ return data;
51
57
  };
52
58
  return {
53
59
  async create({ platform, deviceIdentifier, ...fields }, signal) {
@@ -57,42 +63,75 @@ export function easSessions(credentials) {
57
63
  type: 'AGENT_DEVICE',
58
64
  ...(deviceIdentifier === undefined ? {} : { [platform]: { deviceIdentifier } }),
59
65
  };
60
- const data = await graphql(CREATE, { input }, signal);
61
- const { id, app } = data.deviceRunSession.createDeviceRunSession;
62
- return { id, url: `https://expo.dev/accounts/${encodeURIComponent(app.ownerAccount.name)}/projects/${encodeURIComponent(app.slug)}/simulator-sessions/${id}` };
66
+ const created = field(field(await graphql(CREATE, { input }, signal), 'deviceRunSession'), 'createDeviceRunSession');
67
+ const id = field(created, 'id');
68
+ const app = field(created, 'app');
69
+ const slug = field(app, 'slug');
70
+ const owner = field(field(app, 'ownerAccount'), 'name');
71
+ if (typeof id !== 'string' || typeof slug !== 'string' || typeof owner !== 'string')
72
+ throw unexpected('createDeviceRunSession');
73
+ return { id, url: `https://expo.dev/accounts/${encodeURIComponent(owner)}/projects/${encodeURIComponent(slug)}/simulator-sessions/${id}` };
63
74
  },
64
75
  async state(id, signal) {
65
- const data = await graphql(STATE, { id }, signal);
66
- const session = data.deviceRunSessions.byId;
67
- const job = session.turtleJobRun?.status;
68
- const config = session.remoteConfig;
69
- if (session.status === 'STOPPED' || session.status === 'ERRORED')
70
- return { phase: 'ended', status: session.status.toLowerCase() };
76
+ const session = field(field(await graphql(STATE, { id }, signal), 'deviceRunSessions'), 'byId');
77
+ const status = field(session, 'status');
78
+ const jobRun = field(session, 'turtleJobRun');
79
+ const job = field(jobRun, 'status');
80
+ const config = field(session, 'remoteConfig');
81
+ const daemonUrl = field(config, 'agentDeviceRemoteSessionUrl');
82
+ const daemonToken = field(config, 'agentDeviceRemoteSessionToken');
83
+ const previewUrl = field(config, 'webPreviewUrl');
84
+ const previewToken = field(config, 'webPreviewToken');
85
+ if (typeof status !== 'string' ||
86
+ (jobRun != null && typeof job !== 'string') ||
87
+ (config != null && !isRecord(config)) ||
88
+ ![daemonUrl, daemonToken, previewUrl, previewToken].every((value) => value == null || typeof value === 'string')) {
89
+ throw unexpected('deviceRunSessions.byId');
90
+ }
91
+ if (status === 'STOPPED' || status === 'ERRORED')
92
+ return { phase: 'ended', status: status.toLowerCase() };
71
93
  // A job that ended keeps the daemon address it had, which no longer answers.
72
- if (job !== undefined && ENDED_JOB.has(job))
94
+ if (typeof job === 'string' && ENDED_JOB.has(job))
73
95
  return { phase: 'ended', status: `job ${job.toLowerCase()}` };
74
- if (config?.agentDeviceRemoteSessionUrl !== undefined && config.agentDeviceRemoteSessionToken !== undefined) {
75
- const openPreviewUrl = config.webPreviewToken == null ? (config.webPreviewUrl ?? undefined) : undefined;
76
- return { phase: 'ready', daemonUrl: config.agentDeviceRemoteSessionUrl, daemonToken: config.agentDeviceRemoteSessionToken, openPreviewUrl };
96
+ if (typeof daemonUrl === 'string' && typeof daemonToken === 'string') {
97
+ const openPreviewUrl = previewToken == null && typeof previewUrl === 'string' ? previewUrl : undefined;
98
+ return { phase: 'ready', daemonUrl, daemonToken, openPreviewUrl };
77
99
  }
78
- return { phase: job === undefined || QUEUED_JOB.has(job) ? 'queued' : 'starting' };
100
+ return { phase: typeof job !== 'string' || QUEUED_JOB.has(job) ? 'queued' : 'starting' };
79
101
  },
80
102
  async stop(id, signal) {
81
- let job;
103
+ let data;
82
104
  try {
83
- const data = await graphql(STOP, { id }, signal);
84
- job = data.deviceRunSession.ensureDeviceRunSessionStopped.turtleJobRun;
105
+ data = await graphql(STOP, { id }, signal);
85
106
  }
86
107
  catch (cause) {
87
108
  if (cause instanceof EasError && cause.code === 'NOT_FOUND_ERROR')
88
109
  return;
89
110
  throw cause;
90
111
  }
91
- if (job !== null && QUEUED_JOB.has(job.status))
92
- await graphql(CANCEL_JOB, { id: job.id }, signal);
112
+ const stopped = field(field(data, 'deviceRunSession'), 'ensureDeviceRunSessionStopped');
113
+ if (!isRecord(stopped))
114
+ throw unexpected('ensureDeviceRunSessionStopped');
115
+ const job = stopped['turtleJobRun'];
116
+ if (job === null)
117
+ return;
118
+ const jobId = field(job, 'id');
119
+ const jobStatus = field(job, 'status');
120
+ if (typeof jobId !== 'string' || typeof jobStatus !== 'string')
121
+ throw unexpected('ensureDeviceRunSessionStopped');
122
+ if (QUEUED_JOB.has(jobStatus))
123
+ await graphql(CANCEL_JOB, { id: jobId }, signal);
93
124
  },
94
125
  };
95
126
  }
127
+ /** `key` of a JSON object; `undefined` for a missing key or a value that is not an object. */
128
+ function field(value, key) {
129
+ return isRecord(value) ? value[key] : undefined;
130
+ }
131
+ /** The error for an answer without the fields an operation reads; it names no value, since the answer can hold a daemon token. */
132
+ function unexpected(operation) {
133
+ return new Error(`EAS: unexpected response to ${operation}`);
134
+ }
96
135
  /** An error EAS answered with, its `errorCode` kept. */
97
136
  class EasError extends Error {
98
137
  code;
@@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { envValue } from './env.js';
12
+ import { isRecord } from './json.js';
12
13
  export const EXPO_TOKEN = 'EXPO_TOKEN';
13
14
  /**
14
15
  * `EXPO_TOKEN` from the run's environment, else the `auth.sessionSecret` eas-cli
@@ -36,7 +37,8 @@ export async function expoCredentials(env) {
36
37
  // No cause: a JSON parse error quotes the text around the fault, which can be the secret.
37
38
  throw new Error(`the eas-cli login at ${path} is not valid JSON; run \`eas login\` again, or set ${EXPO_TOKEN}`);
38
39
  }
39
- const sessionSecret = state?.auth?.sessionSecret;
40
+ const auth = isRecord(state) ? state['auth'] : undefined;
41
+ const sessionSecret = isRecord(auth) ? auth['sessionSecret'] : undefined;
40
42
  return typeof sessionSecret === 'string' && sessionSecret.trim() !== '' ? { sessionSecret: sessionSecret.trim() } : undefined;
41
43
  }
42
44
  /** eas-cli's production `state.json` under the run's home: `USERPROFILE` on Windows and `HOME` elsewhere, as `os.homedir()` resolves it. */
package/dist/json.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** A plain JSON object, not an array or `null`. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
package/dist/json.js ADDED
@@ -0,0 +1,4 @@
1
+ /** A plain JSON object, not an array or `null`. */
2
+ export function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
package/dist/project.js CHANGED
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { promisify } from 'node:util';
11
+ import { isRecord } from './json.js';
11
12
  const execFileAsync = promisify(execFile);
12
13
  /** The dynamic app config names Expo resolves, in its order; one of them wins over `app.json`. */
13
14
  const DYNAMIC_CONFIGS = ['app.config.ts', 'app.config.mts', 'app.config.cts', 'app.config.mjs', 'app.config.cjs', 'app.config.js'];
@@ -93,7 +94,3 @@ function projectIdOf(config, source) {
93
94
  return projectId.trim();
94
95
  throw new Error(`${source} has no \`extra.eas.projectId\`; ${PASS_IT}`);
95
96
  }
96
- /** A plain JSON object, not an array or `null`. */
97
- function isRecord(value) {
98
- return typeof value === 'object' && value !== null && !Array.isArray(value);
99
- }
@@ -21,13 +21,14 @@ export interface EasSimulatorsOptions {
21
21
  readonly device?: string | undefined;
22
22
  /**
23
23
  * Minutes without an agent-device command after which EAS stops the
24
- * session. Defaults to 10, or one less than `maxDurationMinutes` when that
24
+ * session. Must be a non-negative integer; zero omits the idle limit.
25
+ * Defaults to 10, or one less than `maxDurationMinutes` when that
25
26
  * is shorter, since EAS wants it below the duration. A session still
26
27
  * queued once another session of the run has been ready for that one's
27
28
  * limit fails the lease: EAS is stopping that one.
28
29
  */
29
30
  readonly maxIdleTimeMinutes?: number | undefined;
30
- /** Minutes a session may run once ready before EAS stops it. Absent, the account's cap: 40, or 115 on a high-priority plan. */
31
+ /** Non-negative integer minutes a session may run once ready before EAS stops it. Absent, the account's cap: 40, or 115 on a high-priority plan. */
31
32
  readonly maxDurationMinutes?: number | undefined;
32
33
  /**
33
34
  * The agent-device version EAS starts the session's daemon at. Defaults to
package/dist/provider.js CHANGED
@@ -52,6 +52,12 @@ const OPTION_KEYS = Object.keys({
52
52
  */
53
53
  export function easSimulators(options = {}) {
54
54
  rejectUnknownKeys('easSimulators()', options, OPTION_KEYS);
55
+ for (const key of ['maxIdleTimeMinutes', 'maxDurationMinutes']) {
56
+ const value = options[key];
57
+ if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
58
+ throw new ConfigurationError('INVALID_CONFIG', `easSimulators: \`${key}\` must be a non-negative integer`);
59
+ }
60
+ }
55
61
  const { projectId, buildId, applicationArchiveUrl, device, maxDurationMinutes, agentDeviceVersion, tags = [] } = options;
56
62
  if (buildId !== undefined && applicationArchiveUrl !== undefined) {
57
63
  throw new ConfigurationError('INVALID_CONFIG', 'easSimulators: pass `buildId` or `applicationArchiveUrl`, not both');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e2e-dev/eas",
3
- "version": "0.3.2-nightly-20261015074742",
3
+ "version": "0.3.2",
4
4
  "description": "EAS Simulators for e2e: run mobile targets on Expo's hosted iOS simulators and Android emulators",
5
5
  "keywords": [
6
6
  "e2e",
@@ -45,13 +45,13 @@
45
45
  "NOTICE"
46
46
  ],
47
47
  "peerDependencies": {
48
- "@e2e-dev/mobile": "0.12.2-nightly-20261015074742",
49
- "e2e": "0.21.1-nightly-20261015074742"
48
+ "@e2e-dev/mobile": ">=0.8.0 <1",
49
+ "e2e": ">=0.15.0 <1"
50
50
  },
51
51
  "devDependencies": {
52
- "@e2e-dev/mobile": "0.12.2-nightly-20261015074742",
52
+ "@e2e-dev/mobile": "0.12.3",
53
53
  "@types/node": "26.6.2",
54
- "e2e": "0.21.1-nightly-20261015074742",
54
+ "e2e": "0.21.2",
55
55
  "typescript": "7.0.2",
56
56
  "vitest": "5.0.1"
57
57
  },