mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a109648a6 | ||
|
|
ea139e207c | ||
|
|
7b3c8cf245 | ||
|
|
efbe0bbdff | ||
|
|
8f645d8870 | ||
|
|
97007d17cc | ||
|
|
0dbaa34a8b | ||
|
|
2fc537f9a7 |
No files matched your search
@@ -8,6 +8,20 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [build-tools] Stream iOS Simulator previews with H.264. ([#4416](https://github.com/expo/eas-cli/pull/4416) by [@gwdp](https://github.com/gwdp))
|
||||
- [build-tools] Report the expo.dev preview page as a simulator session's preview URL, and the preview server behind it as `previewApiUrl`. ([#4353](https://github.com/expo/eas-cli/pull/4353) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] `eas simulator:start` and `eas simulator:get` now report the expo.dev preview page instead of the raw tunnel URL, in both the printed instructions and `--json`. ([#4352](https://github.com/expo/eas-cli/pull/4352) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [build-tools] Honor `EAS_OVERRIDE_PACKAGE_MANAGER` and `EAS_FALLBACK_PACKAGE_MANAGER` when installing and launching simulator packages (serve-sim, device-hub, Argent, agent-device, Appium) so they can use bun instead of stalling on npm audit. ([#4343](https://github.com/expo/eas-cli/pull/4343) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [24.6.0](https://github.com/expo/eas-cli/releases/tag/v24.6.0) - 2026-09-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Display logs in `eas build --wait`. ([#4303](https://github.com/expo/eas-cli/pull/4303) by [@AHGIJMKLKKZNPJKQR](https://github.com/AHGIJMKLKKZNPJKQR))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"STAGING": "24.5.0",
|
||||
"PRODUCTION": "24.5.0"
|
||||
"STAGING": "24.6.0",
|
||||
"PRODUCTION": "24.6.0"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "24.6.0",
|
||||
"version": "24.7.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "24.5.0",
|
||||
"version": "24.7.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -256,7 +256,9 @@ describe('createMaestroTestsBuildFunction', () => {
|
||||
expect(mockedSpawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects sharding with maestro-runner', async () => {
|
||||
it('passes shards to maestro-runner as parallel devices', async () => {
|
||||
mockedSpawn.mockResolvedValue(SPAWN_SUCCESS);
|
||||
jest.spyOn(fs, 'copyFile').mockResolvedValue();
|
||||
const step = createStep({
|
||||
flow_path: ['flows/a.yaml'],
|
||||
platform: 'android',
|
||||
@@ -264,10 +266,9 @@ describe('createMaestroTestsBuildFunction', () => {
|
||||
shards: 2,
|
||||
});
|
||||
|
||||
await expect(step.executeAsync()).rejects.toThrow(
|
||||
'maestro-runner does not support EAS Maestro test sharding'
|
||||
);
|
||||
expect(mockedSpawn).not.toHaveBeenCalled();
|
||||
await step.executeAsync();
|
||||
|
||||
expect(mockedSpawn.mock.calls[0][1]).toContain('--parallel=2');
|
||||
});
|
||||
|
||||
it('rejects a non-junit output_format with maestro-runner', async () => {
|
||||
|
||||
@@ -52,7 +52,11 @@ jest.mock('../../utils/remoteDeviceRunSession', () => ({
|
||||
ensureFfmpegInstalledOnceAsync: jest.fn(),
|
||||
spawnDetached: () => ({ pid: 123, getOutput: () => '', stopAsync: jest.fn() }),
|
||||
waitForFileAsync: async () => ({ port: 1234, token: 'controller-token' }),
|
||||
startNgrokTunnelAsync: async () => ({ url: 'https://controller.test', stopAsync: jest.fn() }),
|
||||
startNgrokTunnelAsync: async () => ({
|
||||
url: 'https://controller.test',
|
||||
subdomainId: 'controller-id',
|
||||
stopAsync: jest.fn(),
|
||||
}),
|
||||
startDeviceWebPreviewWithTunnelAsync: jest.fn(),
|
||||
uploadRemoteSessionConfigAsync: jest.fn(),
|
||||
waitForDeviceRunSessionStoppedAsync: jest.fn(),
|
||||
@@ -115,7 +119,8 @@ describe.each(controllers)('%s local egress', (_name, createFunction, controller
|
||||
jest.mocked(readLocalEgressHandoffAsync).mockResolvedValue(handoff);
|
||||
jest.mocked(monitorLocalEgressAsync).mockResolvedValue(undefined);
|
||||
jest.mocked(startDeviceWebPreviewWithTunnelAsync).mockResolvedValue({
|
||||
previewUrl: 'https://preview.test',
|
||||
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
apiUrl: 'https://preview.test',
|
||||
previewToken: 'preview-secret',
|
||||
stopAsync: stopPreview,
|
||||
});
|
||||
|
||||
+188
-2
@@ -1,9 +1,38 @@
|
||||
import { type bunyan } from '@expo/logger';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { Sentry } from '../../../sentry';
|
||||
import { stopAgentDeviceEventCollectionSafelyAsync } from '../startAgentDeviceRemoteSession';
|
||||
import { spawnDetached } from '../../utils/remoteDeviceRunSession';
|
||||
import {
|
||||
startAgentDeviceDaemonAsync,
|
||||
stopAgentDeviceEventCollectionSafelyAsync,
|
||||
} from '../startAgentDeviceRemoteSession';
|
||||
|
||||
jest.mock('../../../sentry');
|
||||
jest.mock('@expo/turtle-spawn', () => ({ __esModule: true, default: jest.fn() }));
|
||||
jest.mock('../../utils/remoteDeviceRunSession', () => ({
|
||||
...jest.requireActual('../../utils/remoteDeviceRunSession'),
|
||||
spawnDetached: jest.fn(),
|
||||
}));
|
||||
|
||||
const logger = { info: jest.fn(), warn: jest.fn() } as unknown as bunyan;
|
||||
|
||||
async function writeDaemonEntry(cwd: string): Promise<string> {
|
||||
const daemonPath = path.join(
|
||||
cwd,
|
||||
'node_modules',
|
||||
'agent-device',
|
||||
'dist',
|
||||
'src',
|
||||
'internal',
|
||||
'daemon.js'
|
||||
);
|
||||
await fs.promises.mkdir(path.dirname(daemonPath), { recursive: true });
|
||||
await fs.promises.writeFile(daemonPath, '');
|
||||
return daemonPath;
|
||||
}
|
||||
|
||||
describe(stopAgentDeviceEventCollectionSafelyAsync, () => {
|
||||
beforeEach(() => {
|
||||
@@ -12,7 +41,6 @@ describe(stopAgentDeviceEventCollectionSafelyAsync, () => {
|
||||
|
||||
it('reports an unexpected stop failure without rejecting', async () => {
|
||||
const error = new Error('stop failed');
|
||||
const logger = { warn: jest.fn() } as unknown as bunyan;
|
||||
|
||||
await expect(
|
||||
stopAgentDeviceEventCollectionSafelyAsync({
|
||||
@@ -37,3 +65,161 @@ describe(stopAgentDeviceEventCollectionSafelyAsync, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(startAgentDeviceDaemonAsync, () => {
|
||||
const stopAsync = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
stopAsync.mockResolvedValue(undefined);
|
||||
jest.mocked(spawnDetached).mockReturnValue({
|
||||
pid: 1234,
|
||||
getOutput: () => '',
|
||||
stopAsync,
|
||||
});
|
||||
jest.mocked(spawn).mockImplementation((async (
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: { cwd?: string }
|
||||
) => {
|
||||
if (command === 'bun' && args[0] === '--version') {
|
||||
return { stdout: '1.3.11\n' };
|
||||
}
|
||||
if (typeof options?.cwd === 'string' && command !== 'git') {
|
||||
await writeDaemonEntry(options.cwd);
|
||||
}
|
||||
return { stdout: '' };
|
||||
}) as never);
|
||||
});
|
||||
|
||||
it('installs with bun add by default and launches the published daemon', async () => {
|
||||
const handle = await startAgentDeviceDaemonAsync({
|
||||
packageVersion: '1.2.3',
|
||||
env: {},
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'bun',
|
||||
['add', '[email protected]'],
|
||||
expect.objectContaining({ cwd: expect.stringContaining('eas-agent-device-') })
|
||||
);
|
||||
const addCwd = jest.mocked(spawn).mock.calls[0][2]?.cwd as string;
|
||||
expect(spawnDetached).toHaveBeenCalledWith({
|
||||
command: 'node',
|
||||
args: [path.join(addCwd, 'node_modules/agent-device/dist/src/internal/daemon.js')],
|
||||
env: expect.objectContaining({ AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' }),
|
||||
});
|
||||
|
||||
await handle.stopAsync();
|
||||
await expect(fs.promises.access(addCwd)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('installs with npm when EAS_OVERRIDE_PACKAGE_MANAGER is npm', async () => {
|
||||
await startAgentDeviceDaemonAsync({
|
||||
packageVersion: undefined,
|
||||
env: { EAS_OVERRIDE_PACKAGE_MANAGER: 'npm' },
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'npm',
|
||||
['install', '--no-audit', 'agent-device@latest'],
|
||||
expect.objectContaining({ cwd: expect.stringContaining('eas-agent-device-') })
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to git clone when the published daemon is missing', async () => {
|
||||
jest.mocked(spawn).mockImplementation((async (command: string, args: string[]) => {
|
||||
if (command === 'bun' && args[0] === '--version') {
|
||||
return { stdout: '1.3.11\n' };
|
||||
}
|
||||
return { stdout: '' };
|
||||
}) as never);
|
||||
|
||||
await startAgentDeviceDaemonAsync({
|
||||
packageVersion: '1.2.3',
|
||||
env: {},
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'git',
|
||||
[
|
||||
'clone',
|
||||
'--depth',
|
||||
'1',
|
||||
'--branch',
|
||||
'v1.2.3',
|
||||
'https://github.com/callstack/agent-device.git',
|
||||
'/tmp/agent-device-src',
|
||||
],
|
||||
expect.objectContaining({ env: {}, logger })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'bun',
|
||||
['install', '--production'],
|
||||
expect.objectContaining({ cwd: '/tmp/agent-device-src' })
|
||||
);
|
||||
expect(spawnDetached).toHaveBeenCalledWith({
|
||||
command: 'bun',
|
||||
args: ['run', 'src/daemon.ts'],
|
||||
cwd: '/tmp/agent-device-src',
|
||||
env: expect.objectContaining({ AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' }),
|
||||
});
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Failed to start agent-device daemon from the configured package manager; falling back to git clone',
|
||||
expect.any(Error),
|
||||
expect.objectContaining({
|
||||
extras: expect.objectContaining({
|
||||
packageSpec: '[email protected]',
|
||||
packageManager: 'bun',
|
||||
bunVersion: '1.3.11',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('clones latest and reports unknown bun when diagnostics fail', async () => {
|
||||
jest.mocked(spawn).mockImplementation((async (command: string, args: string[]) => {
|
||||
if (command === 'bun' && args[0] === '--version') {
|
||||
throw new Error('bun missing');
|
||||
}
|
||||
return { stdout: '' };
|
||||
}) as never);
|
||||
|
||||
await startAgentDeviceDaemonAsync({
|
||||
packageVersion: undefined,
|
||||
env: { EAS_OVERRIDE_PACKAGE_MANAGER: 'npm' },
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'git',
|
||||
[
|
||||
'clone',
|
||||
'--depth',
|
||||
'1',
|
||||
'https://github.com/callstack/agent-device.git',
|
||||
'/tmp/agent-device-src',
|
||||
],
|
||||
expect.objectContaining({ logger })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'npm',
|
||||
['install', '--no-audit', '--omit=dev'],
|
||||
expect.objectContaining({ cwd: '/tmp/agent-device-src' })
|
||||
);
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Failed to start agent-device daemon from the configured package manager; falling back to git clone',
|
||||
expect.any(Error),
|
||||
expect.objectContaining({
|
||||
extras: expect.objectContaining({
|
||||
packageVersion: 'latest',
|
||||
packageManager: 'npm',
|
||||
bunVersion: 'unknown',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
+68
-1
@@ -1,10 +1,16 @@
|
||||
import { BuildRuntimePlatform } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { AndroidEmulatorUtils } from '../../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils';
|
||||
import { selectXcodeDeveloperDirectoryAsync } from '../../utils/remoteDeviceRunSession';
|
||||
|
||||
import { resolveAppium3VersionSpec, resolveAppiumDeviceAsync } from '../startAppiumRemoteSession';
|
||||
import {
|
||||
installAppiumAsync,
|
||||
resolveAppium3VersionSpec,
|
||||
resolveAppiumDeviceAsync,
|
||||
} from '../startAppiumRemoteSession';
|
||||
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils', () => ({
|
||||
AndroidEmulatorUtils: { getAttachedDevicesAsync: jest.fn() },
|
||||
@@ -15,6 +21,7 @@ jest.mock('../../../utils/IosSimulatorUtils', () => ({
|
||||
jest.mock('../../utils/remoteDeviceRunSession', () => ({
|
||||
selectXcodeDeveloperDirectoryAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('@expo/turtle-spawn', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const logger = { info: jest.fn(), warn: jest.fn() } as never;
|
||||
|
||||
@@ -84,3 +91,63 @@ describe(resolveAppiumDeviceAsync, () => {
|
||||
expect(selectXcodeDeveloperDirectoryAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe(installAppiumAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(spawn).mockImplementation((async (_command: string, args: string[]) => {
|
||||
if (args.includes('--json')) {
|
||||
return { stdout: JSON.stringify({}) };
|
||||
}
|
||||
return { stdout: '' };
|
||||
}) as never);
|
||||
});
|
||||
|
||||
it('installs Appium with npm by default', async () => {
|
||||
const result = await installAppiumAsync({
|
||||
versionSpec: '^3',
|
||||
driverName: 'xcuitest',
|
||||
env: { EXISTING: 'value' },
|
||||
logger,
|
||||
});
|
||||
|
||||
try {
|
||||
expect(spawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'npm',
|
||||
['install', '--no-audit', 'appium@^3'],
|
||||
expect.objectContaining({
|
||||
cwd: result.appiumHome,
|
||||
env: expect.objectContaining({ APPIUM_HOME: result.appiumHome, EXISTING: 'value' }),
|
||||
})
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
result.appiumBinPath,
|
||||
['driver', 'install', 'xcuitest'],
|
||||
expect.objectContaining({ env: result.appiumEnv, logger })
|
||||
);
|
||||
} finally {
|
||||
await fs.promises.rm(result.appiumHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('installs Appium with bun add when EAS_OVERRIDE_PACKAGE_MANAGER is bun', async () => {
|
||||
const result = await installAppiumAsync({
|
||||
versionSpec: '3.5.0',
|
||||
driverName: 'uiautomator2',
|
||||
env: { EAS_OVERRIDE_PACKAGE_MANAGER: 'bun' },
|
||||
logger,
|
||||
});
|
||||
|
||||
try {
|
||||
expect(spawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'bun',
|
||||
['add', '[email protected]'],
|
||||
expect.objectContaining({ cwd: result.appiumHome })
|
||||
);
|
||||
} finally {
|
||||
await fs.promises.rm(result.appiumHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
+5
-2
@@ -88,10 +88,12 @@ describe('createStartArgentRemoteSessionBuildFunction orchestration', () => {
|
||||
});
|
||||
jest.mocked(startNgrokTunnelAsync).mockResolvedValue({
|
||||
url: 'https://argent-abc.tunnel.example.com',
|
||||
subdomainId: 'argent-abc',
|
||||
stopAsync: mockTunnelStopAsync,
|
||||
});
|
||||
jest.mocked(startDeviceWebPreviewWithTunnelAsync).mockResolvedValue({
|
||||
previewUrl: 'https://web-preview.tunnel.example.com',
|
||||
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
apiUrl: 'https://web-preview.tunnel.example.com',
|
||||
stopAsync: mockPreviewStopAsync,
|
||||
});
|
||||
jest.mocked(uploadRemoteSessionConfigAsync).mockResolvedValue(undefined);
|
||||
@@ -179,7 +181,8 @@ describe('createStartArgentRemoteSessionBuildFunction orchestration', () => {
|
||||
expect(uploadRemoteSessionConfigAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
remoteConfig: expect.objectContaining({
|
||||
webPreviewUrl: 'https://web-preview.tunnel.example.com',
|
||||
webPreviewUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
previewApiUrl: 'https://web-preview.tunnel.example.com',
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@ describe(warnIfArgentPackageVersionCannotBeVerified, () => {
|
||||
).not.toThrow();
|
||||
expect(warn).toHaveBeenCalledTimes(2);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Continuing and letting bun x resolve it.')
|
||||
expect.stringContaining('Continuing and letting the package manager resolve it.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,11 @@ jest.mock('../../utils/remoteDeviceRunSession', () => ({
|
||||
|
||||
const logger = { info: jest.fn(), warn: jest.fn() } as unknown as bunyan;
|
||||
const server = { pid: 12345, getOutput: () => '', stopAsync: jest.fn() };
|
||||
const tunnel = { url: 'https://egress.example.com', stopAsync: jest.fn() };
|
||||
const tunnel = {
|
||||
url: 'https://egress.example.com',
|
||||
subdomainId: 'egress-id',
|
||||
stopAsync: jest.fn(),
|
||||
};
|
||||
|
||||
async function start(signal?: AbortSignal): Promise<void> {
|
||||
await createStartLocalEgressBuildFunction().fn!({ logger } as BuildStepContext, {
|
||||
|
||||
+10
-4
@@ -44,7 +44,8 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
|
||||
jest.mocked(getNgrokTunnelDomainOrThrow).mockReturnValue('tunnel.example.com');
|
||||
jest.mocked(selectXcodeDeveloperDirectoryAsync).mockResolvedValue(undefined);
|
||||
jest.mocked(startDeviceWebPreviewWithTunnelAsync).mockResolvedValue({
|
||||
previewUrl: 'https://web-preview.example.test',
|
||||
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
apiUrl: 'https://web-preview.example.test',
|
||||
stopAsync,
|
||||
});
|
||||
jest.mocked(uploadRemoteSessionConfigAsync).mockResolvedValue(undefined);
|
||||
@@ -54,7 +55,8 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
|
||||
|
||||
it('reports the session token when serve-sim minted one', async () => {
|
||||
jest.mocked(startDeviceWebPreviewWithTunnelAsync).mockResolvedValue({
|
||||
previewUrl: 'https://web-preview.example.test',
|
||||
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
apiUrl: 'https://web-preview.example.test',
|
||||
previewToken: 'tok-1',
|
||||
stopAsync,
|
||||
});
|
||||
@@ -64,7 +66,8 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
|
||||
expect(uploadRemoteSessionConfigAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
remoteConfig: {
|
||||
previewUrl: 'https://web-preview.example.test',
|
||||
previewUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
previewApiUrl: 'https://web-preview.example.test',
|
||||
previewToken: 'tok-1',
|
||||
},
|
||||
})
|
||||
@@ -89,7 +92,10 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
|
||||
expect(uploadRemoteSessionConfigAsync).toHaveBeenCalledWith({
|
||||
ctx,
|
||||
deviceRunSessionId: 'device-run-session-id',
|
||||
remoteConfig: { previewUrl: 'https://web-preview.example.test' },
|
||||
remoteConfig: {
|
||||
previewUrl: 'https://expo.dev/simulator-preview/preview-id',
|
||||
previewApiUrl: 'https://web-preview.example.test',
|
||||
},
|
||||
logger,
|
||||
});
|
||||
expect(waitForDeviceRunSessionStoppedAsync).toHaveBeenCalledWith({
|
||||
|
||||
@@ -108,6 +108,9 @@ function buildMaestroArgs({
|
||||
}
|
||||
case 'maestro-runner': {
|
||||
const args = [`--platform=${platform}`, 'test', `--output=${output}`, '--flatten'];
|
||||
if (shards !== undefined && shards > 1) {
|
||||
args.push(`--parallel=${shards}`);
|
||||
}
|
||||
if (includeTags) {
|
||||
args.push(`--include-tags=${includeTags}`);
|
||||
}
|
||||
@@ -259,12 +262,6 @@ export function createMaestroTestsBuildFunction(ctx: CustomBuildContext): BuildF
|
||||
undefined,
|
||||
'android_connection_mode and EAS_MAESTRO_ANDROID_CONNECTION_MODE must be either "adb" or "dadb".'
|
||||
);
|
||||
if (backend === 'maestro-runner' && shards !== undefined && shards > 1) {
|
||||
throw new UserError(
|
||||
'ERR_MAESTRO_INVALID_INPUT',
|
||||
'maestro-runner does not support EAS Maestro test sharding. Remove shards or set it to 1.'
|
||||
);
|
||||
}
|
||||
if (backend === 'maestro-runner' && outputFormat !== undefined && outputFormat !== 'junit') {
|
||||
throw new UserError(
|
||||
'ERR_MAESTRO_INVALID_INPUT',
|
||||
|
||||
@@ -18,6 +18,12 @@ import {
|
||||
withLocalEgressSession,
|
||||
} from '../utils/localEgressSession';
|
||||
import { Sentry } from '../../sentry';
|
||||
import {
|
||||
PackageManager,
|
||||
resolveConfiguredPackageManager,
|
||||
resolvePackageAdd,
|
||||
resolvePackageInstall,
|
||||
} from '../../utils/packageManager';
|
||||
import { pollAgentDeviceArtifactsForUploadAsync } from '../utils/agentDeviceArtifacts';
|
||||
import { startAgentDeviceEventCollectionAsync } from '../utils/agentDeviceEvents';
|
||||
import {
|
||||
@@ -122,7 +128,9 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
logger,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
logger.info(`Web preview URL: ${webPreview.previewUrl}`);
|
||||
logger.info(
|
||||
`Web preview URL: ${webPreview.previewPageUrl} (server: ${webPreview.apiUrl}).`
|
||||
);
|
||||
|
||||
await uploadRemoteSessionConfigWithLocalEgressAsync({
|
||||
env,
|
||||
@@ -132,7 +140,8 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
remoteConfig: {
|
||||
agentDeviceRemoteSessionUrl,
|
||||
agentDeviceRemoteSessionToken: daemonToken,
|
||||
webPreviewUrl: webPreview.previewUrl,
|
||||
webPreviewUrl: webPreview.previewPageUrl,
|
||||
previewApiUrl: webPreview.apiUrl,
|
||||
...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
|
||||
},
|
||||
logger,
|
||||
@@ -183,6 +192,74 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
});
|
||||
}
|
||||
|
||||
export async function startAgentDeviceDaemonAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
packageVersion: string | undefined;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<DetachedProcessHandle> {
|
||||
const packageSpec = createAgentDevicePackageSpec(packageVersion);
|
||||
const packageManager = resolveConfiguredPackageManager(env, PackageManager.BUN);
|
||||
const installDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'eas-agent-device-'));
|
||||
await fs.promises.writeFile(
|
||||
path.join(installDir, 'package.json'),
|
||||
`${JSON.stringify({ name: 'eas-agent-device', private: true })}\n`
|
||||
);
|
||||
|
||||
try {
|
||||
const add = resolvePackageAdd(packageManager, packageSpec);
|
||||
logger.info(`Installing ${packageSpec} with ${add.command}.`);
|
||||
await spawn(add.command, add.args, { cwd: installDir, env, logger });
|
||||
|
||||
const daemonPath = getInstalledAgentDeviceDaemonPath(installDir);
|
||||
if (!fs.existsSync(daemonPath)) {
|
||||
throw new SystemError(`Expected agent-device daemon entry at ${daemonPath}.`);
|
||||
}
|
||||
|
||||
logger.info(`Launching daemon from ${daemonPath} after ${add.command} install.`);
|
||||
const daemonProcess = spawnDetached({
|
||||
command: 'node',
|
||||
args: [daemonPath],
|
||||
env: { ...env, ...AGENT_DEVICE_DAEMON_ENV },
|
||||
});
|
||||
return {
|
||||
...daemonProcess,
|
||||
stopAsync: async () => {
|
||||
await daemonProcess.stopAsync();
|
||||
await fs.promises.rm(installDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
await fs.promises.rm(installDir, { recursive: true, force: true });
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
const bunVersion = await getBunVersionForDiagnosticsAsync(env);
|
||||
Sentry.capture(
|
||||
'Failed to start agent-device daemon from the configured package manager; falling back to git clone',
|
||||
error,
|
||||
{
|
||||
level: 'warning',
|
||||
tags: {
|
||||
phase: 'agent-device-daemon-start',
|
||||
fallback: 'git-clone',
|
||||
},
|
||||
extras: {
|
||||
packageSpec,
|
||||
packageVersion: packageVersion ?? 'latest',
|
||||
packageManager,
|
||||
bunVersion,
|
||||
},
|
||||
}
|
||||
);
|
||||
logger.warn(
|
||||
`Failed to start daemon from ${packageSpec} via ${packageManager}; falling back to git clone: ${error.message}`
|
||||
);
|
||||
return await startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger });
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopAgentDeviceEventCollectionSafelyAsync({
|
||||
eventCollection,
|
||||
deviceRunSessionId,
|
||||
@@ -205,61 +282,6 @@ export async function stopAgentDeviceEventCollectionSafelyAsync({
|
||||
}
|
||||
}
|
||||
|
||||
async function startAgentDeviceDaemonAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
packageVersion: string | undefined;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<DetachedProcessHandle> {
|
||||
const packageSpec = createAgentDevicePackageSpec(packageVersion);
|
||||
try {
|
||||
logger.info(`Installing ${packageSpec} globally with Bun.`);
|
||||
await spawn('bun', ['add', '--global', packageSpec], {
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
const daemonPath = getGlobalAgentDeviceDaemonPath(env);
|
||||
if (!fs.existsSync(daemonPath)) {
|
||||
throw new SystemError(`Expected agent-device daemon entry at ${daemonPath}.`);
|
||||
}
|
||||
|
||||
logger.info(`Launching daemon from ${daemonPath}.`);
|
||||
return spawnDetached({
|
||||
command: 'node',
|
||||
args: [daemonPath],
|
||||
env: { ...env, ...AGENT_DEVICE_DAEMON_ENV },
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
const bunVersion = await getBunVersionForDiagnosticsAsync(env);
|
||||
Sentry.capture(
|
||||
'Failed to start agent-device daemon from global Bun package; falling back to git clone',
|
||||
error,
|
||||
{
|
||||
level: 'warning',
|
||||
tags: {
|
||||
phase: 'agent-device-daemon-start',
|
||||
fallback: 'git-clone',
|
||||
},
|
||||
extras: {
|
||||
packageSpec,
|
||||
packageVersion: packageVersion ?? 'latest',
|
||||
bunVersion,
|
||||
bunInstallConfigured: Boolean(env.BUN_INSTALL?.trim()),
|
||||
},
|
||||
}
|
||||
);
|
||||
logger.warn(
|
||||
`Failed to start daemon from global ${packageSpec}; falling back to git clone: ${error.message}`
|
||||
);
|
||||
return await startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger });
|
||||
}
|
||||
}
|
||||
|
||||
async function startAgentDeviceDaemonFromGitAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
@@ -276,14 +298,17 @@ async function startAgentDeviceDaemonFromGitAsync({
|
||||
);
|
||||
await cloneAgentDeviceAsync({ packageVersion, env, logger });
|
||||
|
||||
logger.info('Installing agent-device dependencies.');
|
||||
await spawn('bun', ['install', '--production'], {
|
||||
const packageManager = resolveConfiguredPackageManager(env, PackageManager.BUN);
|
||||
const install = resolvePackageInstall(packageManager, { production: true });
|
||||
logger.info(`Installing agent-device dependencies with ${install.command}.`);
|
||||
await spawn(install.command, install.args, {
|
||||
cwd: SRC_DIR,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Launching daemon from cloned agent-device source.');
|
||||
// Git fallback is TypeScript source. The published path runs node on dist JS.
|
||||
return spawnDetached({
|
||||
command: 'bun',
|
||||
args: ['run', 'src/daemon.ts'],
|
||||
@@ -346,11 +371,9 @@ function createAgentDevicePackageSpec(packageVersion: string | undefined): strin
|
||||
return `${AGENT_DEVICE_PACKAGE_NAME}@${versionSpec}`;
|
||||
}
|
||||
|
||||
function getGlobalAgentDeviceDaemonPath(env: BuildStepEnv): string {
|
||||
function getInstalledAgentDeviceDaemonPath(installDir: string): string {
|
||||
return path.join(
|
||||
getBunInstallDirectory(env),
|
||||
'install',
|
||||
'global',
|
||||
installDir,
|
||||
'node_modules',
|
||||
AGENT_DEVICE_PACKAGE_NAME,
|
||||
'dist',
|
||||
@@ -360,11 +383,6 @@ function getGlobalAgentDeviceDaemonPath(env: BuildStepEnv): string {
|
||||
);
|
||||
}
|
||||
|
||||
function getBunInstallDirectory(env: BuildStepEnv): string {
|
||||
const bunInstall = env.BUN_INSTALL?.trim();
|
||||
return bunInstall ? bunInstall : path.join(os.homedir(), '.bun');
|
||||
}
|
||||
|
||||
function parseDaemonInfo(raw: string): { port: number; token: string } {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
} from '../utils/localEgressSession';
|
||||
import { AndroidEmulatorUtils } from '../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorUtils } from '../../utils/IosSimulatorUtils';
|
||||
import {
|
||||
PackageManager,
|
||||
resolveConfiguredPackageManager,
|
||||
resolvePackageAdd,
|
||||
} from '../../utils/packageManager';
|
||||
import { sleepAsync } from '../../utils/retry';
|
||||
import { turtleFetch } from '../../utils/turtleFetch';
|
||||
import { startAppiumEventCollectionAsync } from '../utils/appiumEvents';
|
||||
@@ -153,7 +158,8 @@ export function createStartAppiumRemoteSessionBuildFunction(
|
||||
'appium:automationName': device.automationName,
|
||||
'appium:udid': device.udid,
|
||||
},
|
||||
webPreviewUrl: webPreview.previewUrl,
|
||||
webPreviewUrl: webPreview.previewPageUrl,
|
||||
previewApiUrl: webPreview.apiUrl,
|
||||
...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
|
||||
},
|
||||
logger,
|
||||
@@ -198,7 +204,7 @@ export function resolveAppium3VersionSpec(packageVersion: string | undefined): s
|
||||
return versionSpec;
|
||||
}
|
||||
|
||||
type AppiumDevice = {
|
||||
export type AppiumDevice = {
|
||||
platformName: 'iOS' | 'Android';
|
||||
automationName: 'XCUITest' | 'UiAutomator2';
|
||||
driverName: 'xcuitest' | 'uiautomator2';
|
||||
@@ -247,7 +253,7 @@ export async function resolveAppiumDeviceAsync({
|
||||
}
|
||||
}
|
||||
|
||||
async function installAppiumAsync({
|
||||
export async function installAppiumAsync({
|
||||
versionSpec,
|
||||
driverName,
|
||||
env,
|
||||
@@ -265,9 +271,14 @@ async function installAppiumAsync({
|
||||
);
|
||||
const appiumEnv: BuildStepEnv = { ...env, APPIUM_HOME: appiumHome };
|
||||
const appiumBinPath = path.join(appiumHome, 'node_modules', '.bin', 'appium');
|
||||
const add = resolvePackageAdd(
|
||||
resolveConfiguredPackageManager(env, PackageManager.NPM),
|
||||
`appium@${versionSpec}`
|
||||
);
|
||||
|
||||
logger.info(`Installing appium@${versionSpec}.`);
|
||||
await spawn('npm', ['install', '--prefix', appiumHome, `appium@${versionSpec}`], {
|
||||
logger.info(`Installing appium@${versionSpec} with ${add.command}.`);
|
||||
await spawn(add.command, add.args, {
|
||||
cwd: appiumHome,
|
||||
env: appiumEnv,
|
||||
logger,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
withLocalEgressSession,
|
||||
} from '../utils/localEgressSession';
|
||||
import { Sentry } from '../../sentry';
|
||||
import {
|
||||
PackageManager,
|
||||
resolveConfiguredPackageManager,
|
||||
resolvePackageExec,
|
||||
} from '../../utils/packageManager';
|
||||
import { isProcessDescendantOfAsync } from '../../utils/processes';
|
||||
import { sleepAsync } from '../../utils/retry';
|
||||
import { pollArgentArtifactsForUploadAsync } from '../utils/argentArtifacts';
|
||||
@@ -113,41 +118,44 @@ export function createStartArgentRemoteSessionBuildFunction(
|
||||
// Never rejects, so `void` is safe.
|
||||
void ensureFfmpegInstalledOnceAsync({ runtimePlatform, env, logger });
|
||||
|
||||
const packageManager = resolveConfiguredPackageManager(env, PackageManager.BUN);
|
||||
const argentExec = (args: string[]): { command: string; args: string[] } =>
|
||||
resolvePackageExec(packageManager, args);
|
||||
|
||||
logger.info('Enabling the Argent artifacts list endpoint flag.');
|
||||
await spawn(
|
||||
'bun',
|
||||
[
|
||||
'x',
|
||||
`${ARGENT_PACKAGE_NAME}@${versionSpec}`,
|
||||
'enable',
|
||||
ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG,
|
||||
],
|
||||
{ env, logger }
|
||||
);
|
||||
const enableArtifacts = argentExec([
|
||||
`${ARGENT_PACKAGE_NAME}@${versionSpec}`,
|
||||
'enable',
|
||||
ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG,
|
||||
]);
|
||||
await spawn(enableArtifacts.command, enableArtifacts.args, { env, logger });
|
||||
|
||||
logger.info('Enabling the Argent tool-server event log flag.');
|
||||
await spawn(
|
||||
'bun',
|
||||
['x', `${ARGENT_PACKAGE_NAME}@${versionSpec}`, 'enable', ARGENT_EVENT_LOG_FLAG],
|
||||
{ env, logger }
|
||||
);
|
||||
const enableEventLog = argentExec([
|
||||
`${ARGENT_PACKAGE_NAME}@${versionSpec}`,
|
||||
'enable',
|
||||
ARGENT_EVENT_LOG_FLAG,
|
||||
]);
|
||||
await spawn(enableEventLog.command, enableEventLog.args, { env, logger });
|
||||
|
||||
logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} tool-server via bun x.`);
|
||||
// Keep Argent itself in foreground mode under the detached bun process. This preserves
|
||||
// the bun -> Argent CLI -> tool-server ancestry used to identify the matching state file.
|
||||
const startServer = argentExec([
|
||||
`${ARGENT_PACKAGE_NAME}@${versionSpec}`,
|
||||
'server',
|
||||
'start',
|
||||
'--port',
|
||||
'0',
|
||||
'--idle-timeout',
|
||||
'0',
|
||||
'--force',
|
||||
]);
|
||||
logger.info(
|
||||
`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} tool-server via ${startServer.command}.`
|
||||
);
|
||||
// Keep Argent itself in foreground mode under the detached process. This preserves
|
||||
// the npx/bun -> Argent CLI -> tool-server ancestry used to identify the matching state file.
|
||||
const argentServer = spawnDetached({
|
||||
command: 'bun',
|
||||
args: [
|
||||
'x',
|
||||
`${ARGENT_PACKAGE_NAME}@${versionSpec}`,
|
||||
'server',
|
||||
'start',
|
||||
'--port',
|
||||
'0',
|
||||
'--idle-timeout',
|
||||
'0',
|
||||
'--force',
|
||||
],
|
||||
command: startServer.command,
|
||||
args: startServer.args,
|
||||
env: { ...env, ARGENT_EVENT_LOG: ARGENT_EVENT_LOG_PATH },
|
||||
});
|
||||
if (argentServer.pid === undefined) {
|
||||
@@ -216,7 +224,9 @@ export function createStartArgentRemoteSessionBuildFunction(
|
||||
logger,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
logger.info(`Web preview URL: ${webPreview.previewUrl}`);
|
||||
logger.info(
|
||||
`Web preview URL: ${webPreview.previewPageUrl} (server: ${webPreview.apiUrl}).`
|
||||
);
|
||||
|
||||
await uploadRemoteSessionConfigWithLocalEgressAsync({
|
||||
env,
|
||||
@@ -226,7 +236,8 @@ export function createStartArgentRemoteSessionBuildFunction(
|
||||
remoteConfig: {
|
||||
toolsUrl: publicToolsUrl,
|
||||
...(toolServerToken ? { toolsAuthToken: toolServerToken } : {}),
|
||||
webPreviewUrl: webPreview.previewUrl,
|
||||
webPreviewUrl: webPreview.previewPageUrl,
|
||||
previewApiUrl: webPreview.apiUrl,
|
||||
...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
|
||||
},
|
||||
logger,
|
||||
@@ -306,7 +317,7 @@ export function warnIfArgentPackageVersionCannotBeVerified({
|
||||
logger.warn(
|
||||
`Argent remote simulator sessions require ${ARGENT_PACKAGE_NAME}@${MIN_ARGENT_REMOTE_SESSION_VERSION} or newer, ` +
|
||||
`but package_version "${packageVersion}" is not an exact semver version that EAS can verify. ` +
|
||||
`Continuing and letting bun x resolve it.`
|
||||
`Continuing and letting the package manager resolve it.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export function createStartWebPreviewRemoteSessionBuildFunction(
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
packageVersion,
|
||||
});
|
||||
logger.info(`Preview URL: ${webPreview.previewUrl}`);
|
||||
logger.info(`Preview URL: ${webPreview.previewPageUrl} (server: ${webPreview.apiUrl}).`);
|
||||
|
||||
try {
|
||||
await uploadRemoteSessionConfigWithLocalEgressAsync({
|
||||
@@ -70,7 +70,8 @@ export function createStartWebPreviewRemoteSessionBuildFunction(
|
||||
ctx,
|
||||
deviceRunSessionId,
|
||||
remoteConfig: {
|
||||
previewUrl: webPreview.previewUrl,
|
||||
previewUrl: webPreview.previewPageUrl,
|
||||
previewApiUrl: webPreview.apiUrl,
|
||||
...(webPreview.previewToken ? { previewToken: webPreview.previewToken } : {}),
|
||||
},
|
||||
logger,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
turnIceServersToWebPreviewArgs,
|
||||
waitForDeviceRunSessionStoppedAsync,
|
||||
waitForWebPreviewReadyAsync,
|
||||
websiteOrigin,
|
||||
} from '../remoteDeviceRunSession';
|
||||
|
||||
jest.mock('@ngrok/ngrok');
|
||||
@@ -130,7 +131,6 @@ describe(createServeSimArgs, () => {
|
||||
turnArgs: ['--turn-url', 'turns:turn.example.test:443'],
|
||||
})
|
||||
).toEqual([
|
||||
'--yes',
|
||||
'@expo/serve-sim@latest',
|
||||
'--port',
|
||||
'4321',
|
||||
@@ -140,7 +140,7 @@ describe(createServeSimArgs, () => {
|
||||
'--transport',
|
||||
'webrtc',
|
||||
'--webrtc-codec',
|
||||
'vp8',
|
||||
'h264',
|
||||
'--max-dimension',
|
||||
'960',
|
||||
'--mjpeg-quality',
|
||||
@@ -169,17 +169,15 @@ describe(createServeSimArgs, () => {
|
||||
});
|
||||
|
||||
it('pins the requested package version', () => {
|
||||
expect(createServeSimArgs({ port: 4321, packageVersion: '0.1.38' }).slice(0, 2)).toEqual([
|
||||
'--yes',
|
||||
'@expo/[email protected]',
|
||||
]);
|
||||
expect(createServeSimArgs({ port: 4321, packageVersion: '0.1.38' })[0]).toBe(
|
||||
'@expo/[email protected]'
|
||||
);
|
||||
});
|
||||
|
||||
it('pins a dist-tag', () => {
|
||||
expect(createServeSimArgs({ port: 4321, packageVersion: 'next' }).slice(0, 2)).toEqual([
|
||||
'--yes',
|
||||
'@expo/serve-sim@next',
|
||||
]);
|
||||
expect(createServeSimArgs({ port: 4321, packageVersion: 'next' })[0]).toBe(
|
||||
'@expo/serve-sim@next'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,7 +189,6 @@ describe(createExpoDeviceHubArgs, () => {
|
||||
turnArgs: ['--turn-url', 'turns:turn.example.test:443'],
|
||||
})
|
||||
).toEqual([
|
||||
'--yes',
|
||||
'expo-device-hub@latest',
|
||||
'--port',
|
||||
'4321',
|
||||
@@ -219,10 +216,9 @@ describe(createExpoDeviceHubArgs, () => {
|
||||
});
|
||||
|
||||
it('pins the requested package version', () => {
|
||||
expect(createExpoDeviceHubArgs({ port: 4321, packageVersion: '0.7.0' }).slice(0, 2)).toEqual([
|
||||
'--yes',
|
||||
'[email protected]',
|
||||
]);
|
||||
expect(createExpoDeviceHubArgs({ port: 4321, packageVersion: '0.7.0' })[0]).toBe(
|
||||
'[email protected]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,6 +305,11 @@ describe(startNgrokTunnelAsync, () => {
|
||||
})
|
||||
);
|
||||
expect(tunnel.url).toBe('https://web-preview.example.test');
|
||||
expect(ngrok.forward).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
domain: `web-preview-${tunnel.subdomainId}.eas-simulator.ngrok.dev`,
|
||||
})
|
||||
);
|
||||
await tunnel.stopAsync();
|
||||
await tunnel.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
@@ -419,9 +420,9 @@ describe(startDeviceWebPreviewWithTunnelAsync, () => {
|
||||
const port = Number(args[args.indexOf('--port') + 1]);
|
||||
expect(port).toBeGreaterThan(0);
|
||||
expect(command).toBe('npx');
|
||||
expect(args).toEqual(createExpoDeviceHubArgs({ port, turnArgs, packageVersion }));
|
||||
expect(args).toEqual(['--yes', ...createExpoDeviceHubArgs({ port, turnArgs, packageVersion })]);
|
||||
expect(ngrok.forward).toHaveBeenCalledWith(expect.objectContaining({ addr: port }));
|
||||
expect(preview.previewUrl).toBe('https://android-preview.example.test');
|
||||
expect(preview.apiUrl).toBe('https://android-preview.example.test');
|
||||
|
||||
await preview.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
@@ -442,7 +443,38 @@ describe(startDeviceWebPreviewWithTunnelAsync, () => {
|
||||
});
|
||||
|
||||
expect(preview.previewToken).toBe('tok-1');
|
||||
expect(preview.previewUrl).toBe('https://preview.example.test');
|
||||
expect(preview.apiUrl).toBe('https://preview.example.test');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, 'https://expo.dev'],
|
||||
[{ EXPO_STAGING: '1' }, 'https://staging.expo.dev'],
|
||||
[{ EXPO_LOCAL: '1' }, 'https://expo.test'],
|
||||
])('points the preview page at the website for the stage the worker runs in', (stage, origin) => {
|
||||
expect(websiteOrigin({ ...env, ...stage })).toBe(origin);
|
||||
});
|
||||
|
||||
it('points the preview URL at the website page for the tunnel', async () => {
|
||||
jest.mocked(ngrok.forward).mockResolvedValue({
|
||||
url: () => 'https://preview.example.test',
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
|
||||
const preview = await startDeviceWebPreviewWithTunnelAsync(createCtxMock(), {
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
baseDomain,
|
||||
env: { ...env, EXPO_STAGING: '1' },
|
||||
logger: createLoggerMock(),
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
expect(preview.previewPageUrl).toMatch(
|
||||
/^https:\/\/staging\.expo\.dev\/simulator-preview\/[a-f0-9]{32}$/
|
||||
);
|
||||
const previewId = preview.previewPageUrl.split('/').at(-1);
|
||||
expect(ngrok.forward).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ domain: `web-preview-${previewId}.${baseDomain}` })
|
||||
);
|
||||
});
|
||||
|
||||
// serve-sim is always launched with --require-token, so a missing token means it is running
|
||||
@@ -505,9 +537,18 @@ describe(startDeviceWebPreviewWithTunnelAsync, () => {
|
||||
const port = Number(args[args.indexOf('--port') + 1]);
|
||||
expect(port).toBeGreaterThan(0);
|
||||
expect(command).toBe('npx');
|
||||
expect(args).toEqual(createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion }));
|
||||
expect(args).toEqual([
|
||||
'--yes',
|
||||
...createServeSimArgs({
|
||||
port,
|
||||
turnArgs,
|
||||
metricsCorsArgs,
|
||||
frameAncestorArgs: ['--frame-ancestor', 'https://expo.dev'],
|
||||
packageVersion,
|
||||
}),
|
||||
]);
|
||||
expect(ngrok.forward).toHaveBeenCalledWith(expect.objectContaining({ addr: port }));
|
||||
expect(preview.previewUrl).toBe('https://ios-preview.example.test');
|
||||
expect(preview.apiUrl).toBe('https://ios-preview.example.test');
|
||||
|
||||
await preview.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
@@ -538,6 +579,73 @@ describe(startDeviceWebPreviewWithTunnelAsync, () => {
|
||||
await preview.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('launches serve-sim with bun x when EAS_OVERRIDE_PACKAGE_MANAGER is bun', async () => {
|
||||
const close = jest.fn().mockResolvedValue(undefined);
|
||||
jest.mocked(ngrok.forward).mockResolvedValue({
|
||||
url: () => 'https://ios-preview.example.test',
|
||||
close,
|
||||
} as never);
|
||||
|
||||
const bunEnv = { ...env, EAS_OVERRIDE_PACKAGE_MANAGER: 'bun' };
|
||||
const preview = await startDeviceWebPreviewWithTunnelAsync(createCtxMock(), {
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
baseDomain,
|
||||
env: bunEnv,
|
||||
logger: createLoggerMock(),
|
||||
timeoutMs: 10_000,
|
||||
packageVersion: '4.5.6',
|
||||
});
|
||||
|
||||
const [command, args] = jest.mocked(spawn).mock.calls[0];
|
||||
const port = Number(args[args.indexOf('--port') + 1]);
|
||||
expect(command).toBe('bun');
|
||||
expect(args).toEqual([
|
||||
'x',
|
||||
...createServeSimArgs({
|
||||
port,
|
||||
turnArgs,
|
||||
metricsCorsArgs,
|
||||
frameAncestorArgs: ['--frame-ancestor', 'https://expo.dev'],
|
||||
packageVersion: '4.5.6',
|
||||
}),
|
||||
]);
|
||||
|
||||
await preview.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('launches serve-sim with bun x when EAS_FALLBACK_PACKAGE_MANAGER is bun', async () => {
|
||||
const close = jest.fn().mockResolvedValue(undefined);
|
||||
jest.mocked(ngrok.forward).mockResolvedValue({
|
||||
url: () => 'https://ios-preview.example.test',
|
||||
close,
|
||||
} as never);
|
||||
|
||||
const preview = await startDeviceWebPreviewWithTunnelAsync(createCtxMock(), {
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
baseDomain,
|
||||
env: { ...env, EAS_FALLBACK_PACKAGE_MANAGER: 'bun' },
|
||||
logger: createLoggerMock(),
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
const [command, args] = jest.mocked(spawn).mock.calls[0];
|
||||
const port = Number(args[args.indexOf('--port') + 1]);
|
||||
expect(command).toBe('bun');
|
||||
expect(args).toEqual([
|
||||
'x',
|
||||
...createServeSimArgs({
|
||||
port,
|
||||
turnArgs,
|
||||
metricsCorsArgs,
|
||||
frameAncestorArgs: ['--frame-ancestor', 'https://expo.dev'],
|
||||
}),
|
||||
]);
|
||||
|
||||
await preview.stopAsync();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(turnIceServersToWebPreviewArgs, () => {
|
||||
|
||||
@@ -15,6 +15,11 @@ import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import { Sentry } from '../../sentry';
|
||||
import {
|
||||
PackageManager,
|
||||
resolveConfiguredPackageManager,
|
||||
resolvePackageExec,
|
||||
} from '../../utils/packageManager';
|
||||
import { sleepAsync } from '../../utils/retry';
|
||||
import { turtleFetch } from '../../utils/turtleFetch';
|
||||
import { SERVE_SIM_STATE_DIR, readServeSimServersAsync } from './serveSimMetricsRecorder';
|
||||
@@ -22,6 +27,9 @@ import { SERVE_SIM_STATE_DIR, readServeSimServersAsync } from './serveSimMetrics
|
||||
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
||||
const WEB_PREVIEW_HOST = '127.0.0.1';
|
||||
const SERVE_SIM_PACKAGE_NAME = '@expo/serve-sim';
|
||||
// Pinned while H.264 is known to stall at larger encode sizes without recovering.
|
||||
// Passing this explicitly also overrides serve-sim's own default H.264 cap, so raising it
|
||||
// loses that guard too.
|
||||
const SERVE_SIM_MAX_DIMENSION = '960';
|
||||
const SERVE_SIM_MJPEG_QUALITY = '0.55';
|
||||
const SERVE_SIM_VIDEO_BITRATE = '6000000';
|
||||
@@ -549,7 +557,7 @@ async function stopDetachedProcessAsync(pid: number | undefined): Promise<void>
|
||||
}
|
||||
try {
|
||||
// spawnDetached creates a dedicated process group. Signaling the group also
|
||||
// terminates npx/bunx descendants instead of leaving the actual daemon alive.
|
||||
// terminates npx/bun descendants instead of leaving the actual daemon alive.
|
||||
process.kill(-pid, 'SIGTERM');
|
||||
} catch {
|
||||
try {
|
||||
@@ -612,6 +620,14 @@ export function spawnDetached({
|
||||
};
|
||||
}
|
||||
|
||||
export function websiteOrigin(env: BuildStepEnv): string {
|
||||
return env.EXPO_LOCAL
|
||||
? 'https://expo.test'
|
||||
: env.EXPO_STAGING
|
||||
? 'https://staging.expo.dev'
|
||||
: 'https://expo.dev';
|
||||
}
|
||||
|
||||
export function metricsCorsOriginToServeSimArgs(env: BuildStepEnv): string[] {
|
||||
const origin = env.EAS_SIMULATOR_METRICS_CORS_ORIGIN;
|
||||
if (!origin) {
|
||||
@@ -639,15 +655,16 @@ export function createServeSimArgs({
|
||||
port,
|
||||
turnArgs = [],
|
||||
metricsCorsArgs = [],
|
||||
frameAncestorArgs = [],
|
||||
packageVersion,
|
||||
}: {
|
||||
port: number;
|
||||
turnArgs?: string[];
|
||||
metricsCorsArgs?: string[];
|
||||
frameAncestorArgs?: string[];
|
||||
packageVersion?: string;
|
||||
}): string[] {
|
||||
return [
|
||||
'--yes',
|
||||
createServeSimPackageSpec(packageVersion),
|
||||
'--port',
|
||||
String(port),
|
||||
@@ -657,7 +674,7 @@ export function createServeSimArgs({
|
||||
'--transport',
|
||||
'webrtc',
|
||||
'--webrtc-codec',
|
||||
'vp8',
|
||||
'h264',
|
||||
'--max-dimension',
|
||||
SERVE_SIM_MAX_DIMENSION,
|
||||
'--mjpeg-quality',
|
||||
@@ -668,6 +685,7 @@ export function createServeSimArgs({
|
||||
SERVE_SIM_VIDEO_FPS,
|
||||
...turnArgs,
|
||||
...metricsCorsArgs,
|
||||
...frameAncestorArgs,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -681,7 +699,6 @@ export function createExpoDeviceHubArgs({
|
||||
packageVersion?: string;
|
||||
}): string[] {
|
||||
return [
|
||||
'--yes',
|
||||
createExpoDeviceHubPackageSpec(packageVersion),
|
||||
'--port',
|
||||
String(port),
|
||||
@@ -771,7 +788,8 @@ export async function waitForWebPreviewReadyAsync({
|
||||
}
|
||||
|
||||
export type DeviceWebPreviewHandle = {
|
||||
previewUrl: string;
|
||||
previewPageUrl: string;
|
||||
apiUrl: string;
|
||||
/** Session token gating the preview. Only serve-sim mints one. */
|
||||
previewToken?: string;
|
||||
stopAsync: () => Promise<void>;
|
||||
@@ -802,11 +820,17 @@ async function startWebPreviewWithTunnelAsync(
|
||||
}
|
||||
): Promise<DeviceWebPreviewHandle> {
|
||||
const port = await findAvailablePortAsync();
|
||||
logger.info(`Launching ${packageSpec} on ${WEB_PREVIEW_HOST}:${port}.`);
|
||||
const turnArgs = await fetchWebPreviewTurnArgsAsync(ctx, { env, logger });
|
||||
const previewExec = resolvePackageExec(
|
||||
resolveConfiguredPackageManager(env, PackageManager.NPM),
|
||||
createArgs(port, turnArgs)
|
||||
);
|
||||
logger.info(
|
||||
`Launching ${packageSpec} on ${WEB_PREVIEW_HOST}:${port} via ${previewExec.command}.`
|
||||
);
|
||||
const previewServer = spawnDetached({
|
||||
command: 'npx',
|
||||
args: createArgs(port, turnArgs),
|
||||
command: previewExec.command,
|
||||
args: previewExec.args,
|
||||
env,
|
||||
});
|
||||
|
||||
@@ -827,7 +851,11 @@ async function startWebPreviewWithTunnelAsync(
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
previewUrl: tunnel.url,
|
||||
previewPageUrl: new URL(
|
||||
`/simulator-preview/${tunnel.subdomainId}`,
|
||||
websiteOrigin(env)
|
||||
).toString(),
|
||||
apiUrl: tunnel.url,
|
||||
previewToken,
|
||||
stopAsync: async () => {
|
||||
const results = await Promise.allSettled([tunnel.stopAsync(), previewServer.stopAsync()]);
|
||||
@@ -869,6 +897,7 @@ export async function startServeSimWithTunnelAsync(
|
||||
}
|
||||
): Promise<ServeSimPreviewHandle> {
|
||||
const metricsCorsArgs = metricsCorsOriginToServeSimArgs(env);
|
||||
const frameAncestorArgs = ['--frame-ancestor', websiteOrigin(env)];
|
||||
return await startWebPreviewWithTunnelAsync(ctx, {
|
||||
baseDomain,
|
||||
env,
|
||||
@@ -877,7 +906,7 @@ export async function startServeSimWithTunnelAsync(
|
||||
serverName: 'serve-sim',
|
||||
packageSpec: createServeSimPackageSpec(packageVersion),
|
||||
createArgs: (port, turnArgs) =>
|
||||
createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion }),
|
||||
createServeSimArgs({ port, turnArgs, metricsCorsArgs, frameAncestorArgs, packageVersion }),
|
||||
readPreviewTokenAsync: async device => {
|
||||
const previewToken = await readServeSimPreviewTokenAsync(device);
|
||||
if (!previewToken) {
|
||||
@@ -951,6 +980,7 @@ export async function startDeviceWebPreviewWithTunnelAsync(
|
||||
|
||||
export type NgrokTunnelHandle = {
|
||||
url: string;
|
||||
subdomainId: string;
|
||||
stopAsync: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -969,7 +999,8 @@ export async function startNgrokTunnelAsync({
|
||||
rewriteHostHeader?: boolean;
|
||||
logger: bunyan;
|
||||
}): Promise<NgrokTunnelHandle> {
|
||||
const domain = `${subdomainPrefix}-${randomBytes(16).toString('hex')}.${baseDomain}`;
|
||||
const subdomainId = randomBytes(16).toString('hex');
|
||||
const domain = `${subdomainPrefix}-${subdomainId}.${baseDomain}`;
|
||||
logger.info(`Starting ngrok tunnel ${domain} -> http://localhost:${port}.`);
|
||||
// Run the ngrok agent in-process via the SDK; it keeps the session alive until
|
||||
// the process exits, and the step blocks forever to hold it open.
|
||||
@@ -987,6 +1018,7 @@ export async function startNgrokTunnelAsync({
|
||||
let stopped = false;
|
||||
return {
|
||||
url,
|
||||
subdomainId,
|
||||
stopAsync: async () => {
|
||||
if (stopped) {
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
PackageManager,
|
||||
findPackagerRootDir,
|
||||
getPackageVersionFromPackageJson,
|
||||
resolveConfiguredPackageManager,
|
||||
resolveFallbackPackageManager,
|
||||
resolveOverridePackageManager,
|
||||
resolvePackageAdd,
|
||||
resolvePackageExec,
|
||||
resolvePackageInstall,
|
||||
resolvePackageManager,
|
||||
resolvePackageVersionAsync,
|
||||
shouldUseFrozenLockfile,
|
||||
@@ -117,6 +123,176 @@ describe(resolvePackageManager, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveFallbackPackageManager, () => {
|
||||
it('returns undefined when unset or empty', () => {
|
||||
expect(resolveFallbackPackageManager({})).toBeUndefined();
|
||||
expect(resolveFallbackPackageManager({ EAS_FALLBACK_PACKAGE_MANAGER: '' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns bun when set', () => {
|
||||
expect(resolveFallbackPackageManager({ EAS_FALLBACK_PACKAGE_MANAGER: 'bun' })).toBe(
|
||||
PackageManager.BUN
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a UserError on an unsupported value', () => {
|
||||
expect(() => resolveFallbackPackageManager({ EAS_FALLBACK_PACKAGE_MANAGER: 'bunn' })).toThrow(
|
||||
errors.UserError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveOverridePackageManager, () => {
|
||||
it('returns undefined when unset or empty', () => {
|
||||
expect(resolveOverridePackageManager({})).toBeUndefined();
|
||||
expect(resolveOverridePackageManager({ EAS_OVERRIDE_PACKAGE_MANAGER: '' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns bun when set', () => {
|
||||
expect(resolveOverridePackageManager({ EAS_OVERRIDE_PACKAGE_MANAGER: 'bun' })).toBe(
|
||||
PackageManager.BUN
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a UserError on an unsupported value', () => {
|
||||
expect(() => resolveOverridePackageManager({ EAS_OVERRIDE_PACKAGE_MANAGER: 'bunn' })).toThrow(
|
||||
errors.UserError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveConfiguredPackageManager, () => {
|
||||
it('uses the unset default', () => {
|
||||
expect(resolveConfiguredPackageManager({}, PackageManager.NPM)).toBe(PackageManager.NPM);
|
||||
});
|
||||
|
||||
it('uses EAS_FALLBACK_PACKAGE_MANAGER when no override is set', () => {
|
||||
expect(
|
||||
resolveConfiguredPackageManager({ EAS_FALLBACK_PACKAGE_MANAGER: 'bun' }, PackageManager.NPM)
|
||||
).toBe(PackageManager.BUN);
|
||||
});
|
||||
|
||||
it('prefers EAS_OVERRIDE_PACKAGE_MANAGER over the fallback', () => {
|
||||
expect(
|
||||
resolveConfiguredPackageManager(
|
||||
{
|
||||
EAS_OVERRIDE_PACKAGE_MANAGER: 'npm',
|
||||
EAS_FALLBACK_PACKAGE_MANAGER: 'bun',
|
||||
},
|
||||
PackageManager.BUN
|
||||
)
|
||||
).toBe(PackageManager.NPM);
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolvePackageExec, () => {
|
||||
const packageArgs = ['@expo/serve-sim@latest', '--port', '1'];
|
||||
|
||||
it('maps npm to npx --yes', () => {
|
||||
expect(resolvePackageExec(PackageManager.NPM, packageArgs)).toEqual({
|
||||
command: 'npx',
|
||||
args: ['--yes', ...packageArgs],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps bun to bun x', () => {
|
||||
expect(resolvePackageExec(PackageManager.BUN, packageArgs)).toEqual({
|
||||
command: 'bun',
|
||||
args: ['x', ...packageArgs],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps yarn to npx --yes', () => {
|
||||
expect(resolvePackageExec(PackageManager.YARN, packageArgs)).toEqual({
|
||||
command: 'npx',
|
||||
args: ['--yes', ...packageArgs],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps pnpm to pnpm dlx', () => {
|
||||
expect(resolvePackageExec(PackageManager.PNPM, packageArgs)).toEqual({
|
||||
command: 'pnpm',
|
||||
args: ['dlx', ...packageArgs],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolvePackageAdd, () => {
|
||||
it('maps npm to npm install --no-audit', () => {
|
||||
expect(resolvePackageAdd(PackageManager.NPM, 'appium@^3')).toEqual({
|
||||
command: 'npm',
|
||||
args: ['install', '--no-audit', 'appium@^3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps bun to bun add', () => {
|
||||
expect(resolvePackageAdd(PackageManager.BUN, 'agent-device@latest')).toEqual({
|
||||
command: 'bun',
|
||||
args: ['add', 'agent-device@latest'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps yarn to yarn add', () => {
|
||||
expect(resolvePackageAdd(PackageManager.YARN, 'appium@^3')).toEqual({
|
||||
command: 'yarn',
|
||||
args: ['add', 'appium@^3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps pnpm to pnpm add', () => {
|
||||
expect(resolvePackageAdd(PackageManager.PNPM, 'appium@^3')).toEqual({
|
||||
command: 'pnpm',
|
||||
args: ['add', 'appium@^3'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolvePackageInstall, () => {
|
||||
it('maps bun production install', () => {
|
||||
expect(resolvePackageInstall(PackageManager.BUN, { production: true })).toEqual({
|
||||
command: 'bun',
|
||||
args: ['install', '--production'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps npm production install without audit', () => {
|
||||
expect(resolvePackageInstall(PackageManager.NPM, { production: true })).toEqual({
|
||||
command: 'npm',
|
||||
args: ['install', '--no-audit', '--omit=dev'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps yarn and pnpm production installs', () => {
|
||||
expect(resolvePackageInstall(PackageManager.YARN, { production: true })).toEqual({
|
||||
command: 'yarn',
|
||||
args: ['install', '--production'],
|
||||
});
|
||||
expect(resolvePackageInstall(PackageManager.PNPM, { production: true })).toEqual({
|
||||
command: 'pnpm',
|
||||
args: ['install', '--prod'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps non-production installs', () => {
|
||||
expect(resolvePackageInstall(PackageManager.NPM)).toEqual({
|
||||
command: 'npm',
|
||||
args: ['install', '--no-audit'],
|
||||
});
|
||||
expect(resolvePackageInstall(PackageManager.BUN)).toEqual({
|
||||
command: 'bun',
|
||||
args: ['install'],
|
||||
});
|
||||
expect(resolvePackageInstall(PackageManager.YARN)).toEqual({
|
||||
command: 'yarn',
|
||||
args: ['install'],
|
||||
});
|
||||
expect(resolvePackageInstall(PackageManager.PNPM)).toEqual({
|
||||
command: 'pnpm',
|
||||
args: ['install'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolvePackageVersionAsync, () => {
|
||||
const logger = {
|
||||
debug: jest.fn(),
|
||||
|
||||
@@ -30,19 +30,100 @@ export function resolvePackageManager(
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const fallback = env.EAS_FALLBACK_PACKAGE_MANAGER;
|
||||
if (fallback) {
|
||||
const parsed = z.enum(PackageManager).safeParse(fallback);
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
const allowed = Object.values(PackageManager).join(', ');
|
||||
throw new errors.UserError(
|
||||
'EAS_INVALID_FALLBACK_PACKAGE_MANAGER',
|
||||
`Invalid EAS_FALLBACK_PACKAGE_MANAGER value "${fallback}" (expected one of: ${allowed}).`
|
||||
);
|
||||
return resolveFallbackPackageManager(env) ?? PackageManager.YARN;
|
||||
}
|
||||
|
||||
function parsePackageManagerEnvValue(
|
||||
value: string | undefined,
|
||||
envName: string,
|
||||
errorCode: string
|
||||
): PackageManager | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = z.enum(PackageManager).safeParse(value);
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
const allowed = Object.values(PackageManager).join(', ');
|
||||
throw new errors.UserError(
|
||||
errorCode,
|
||||
`Invalid ${envName} value "${value}" (expected one of: ${allowed}).`
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveFallbackPackageManager(env: BuildStepEnv): PackageManager | undefined {
|
||||
return parsePackageManagerEnvValue(
|
||||
env.EAS_FALLBACK_PACKAGE_MANAGER,
|
||||
'EAS_FALLBACK_PACKAGE_MANAGER',
|
||||
'EAS_INVALID_FALLBACK_PACKAGE_MANAGER'
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveOverridePackageManager(env: BuildStepEnv): PackageManager | undefined {
|
||||
return parsePackageManagerEnvValue(
|
||||
env.EAS_OVERRIDE_PACKAGE_MANAGER,
|
||||
'EAS_OVERRIDE_PACKAGE_MANAGER',
|
||||
'EAS_INVALID_OVERRIDE_PACKAGE_MANAGER'
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveConfiguredPackageManager(
|
||||
env: BuildStepEnv,
|
||||
whenUnset: PackageManager
|
||||
): PackageManager {
|
||||
return resolveOverridePackageManager(env) ?? resolveFallbackPackageManager(env) ?? whenUnset;
|
||||
}
|
||||
|
||||
export function resolvePackageExec(
|
||||
manager: PackageManager,
|
||||
args: string[]
|
||||
): { command: string; args: string[] } {
|
||||
switch (manager) {
|
||||
case PackageManager.NPM:
|
||||
return { command: 'npx', args: ['--yes', ...args] };
|
||||
case PackageManager.BUN:
|
||||
return { command: 'bun', args: ['x', ...args] };
|
||||
case PackageManager.YARN:
|
||||
return { command: 'npx', args: ['--yes', ...args] };
|
||||
case PackageManager.PNPM:
|
||||
return { command: 'pnpm', args: ['dlx', ...args] };
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePackageAdd(
|
||||
manager: PackageManager,
|
||||
packageSpec: string
|
||||
): { command: string; args: string[] } {
|
||||
switch (manager) {
|
||||
case PackageManager.NPM:
|
||||
return { command: 'npm', args: ['install', '--no-audit', packageSpec] };
|
||||
case PackageManager.BUN:
|
||||
return { command: 'bun', args: ['add', packageSpec] };
|
||||
case PackageManager.YARN:
|
||||
return { command: 'yarn', args: ['add', packageSpec] };
|
||||
case PackageManager.PNPM:
|
||||
return { command: 'pnpm', args: ['add', packageSpec] };
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePackageInstall(
|
||||
manager: PackageManager,
|
||||
{ production }: { production?: boolean } = {}
|
||||
): { command: string; args: string[] } {
|
||||
switch (manager) {
|
||||
case PackageManager.NPM:
|
||||
return {
|
||||
command: 'npm',
|
||||
args: production ? ['install', '--no-audit', '--omit=dev'] : ['install', '--no-audit'],
|
||||
};
|
||||
case PackageManager.BUN:
|
||||
return { command: 'bun', args: production ? ['install', '--production'] : ['install'] };
|
||||
case PackageManager.YARN:
|
||||
return { command: 'yarn', args: production ? ['install', '--production'] : ['install'] };
|
||||
case PackageManager.PNPM:
|
||||
return { command: 'pnpm', args: production ? ['install', '--prod'] : ['install'] };
|
||||
}
|
||||
return PackageManager.YARN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+133
-133
File diff suppressed because it is too large.
Load diff
Generated
+60
@@ -6680,6 +6680,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewApiUrl",
|
||||
"description": "URL of the preview server, for reaching its API rather than its page.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPreviewToken",
|
||||
"description": "Session token gating the web preview. Null when the preview runs ungated.",
|
||||
@@ -33439,6 +33451,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewApiUrl",
|
||||
"description": "URL of the preview server, for reaching its API rather than its page.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPreviewToken",
|
||||
"description": "Session token gating the web preview. Null when the preview runs ungated.",
|
||||
@@ -36300,6 +36324,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewApiUrl",
|
||||
"description": "URL of the preview server, for reaching its API rather than its page.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "toolsAuthToken",
|
||||
"description": null,
|
||||
@@ -78928,6 +78964,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewApiUrl",
|
||||
"description": "URL of the preview server, for reaching its API rather than its page.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewToken",
|
||||
"description": "Session token gating the preview. Null when the preview runs ungated.",
|
||||
@@ -93670,6 +93718,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "previewApiUrl",
|
||||
"description": "URL of the preview server, for reaching its API rather than its page.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPreviewToken",
|
||||
"description": "Session token gating the web preview. Null when the preview runs ungated.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "24.6.0",
|
||||
"version": "24.7.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
+15
-5
@@ -1102,6 +1102,8 @@ export type AgentDeviceRunSessionRemoteConfig = {
|
||||
*/
|
||||
egressUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Session token gating the web preview. Null when the preview runs ungated. */
|
||||
/** URL of the preview server, for reaching its API rather than its page. */
|
||||
previewApiUrl?: Maybe<Scalars['String']['output']>;
|
||||
webPreviewToken?: Maybe<Scalars['String']['output']>;
|
||||
/**
|
||||
* URL of the web preview surface for the session. Null when a web preview is
|
||||
@@ -4720,6 +4722,8 @@ export type AppiumRunSessionRemoteConfig = {
|
||||
*/
|
||||
egressUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Session token gating the web preview. Null when the preview runs ungated. */
|
||||
/** URL of the preview server, for reaching its API rather than its page. */
|
||||
previewApiUrl?: Maybe<Scalars['String']['output']>;
|
||||
webPreviewToken?: Maybe<Scalars['String']['output']>;
|
||||
/**
|
||||
* URL of the web preview surface for the session. Null when a web preview is
|
||||
@@ -5147,6 +5151,8 @@ export type ArgentRunSessionRemoteConfig = {
|
||||
* session does not use local egress.
|
||||
*/
|
||||
egressUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** URL of the preview server, for reaching its API rather than its page. */
|
||||
previewApiUrl?: Maybe<Scalars['String']['output']>;
|
||||
toolsAuthToken?: Maybe<Scalars['String']['output']>;
|
||||
toolsUrl: Scalars['String']['output'];
|
||||
/** Session token gating the web preview. Null when the preview runs ungated. */
|
||||
@@ -11214,6 +11220,8 @@ export type ServeSimRunSessionRemoteConfig = {
|
||||
*/
|
||||
egressUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Session token gating the preview. Null when the preview runs ungated. */
|
||||
/** URL of the preview server, for reaching its API rather than its page. */
|
||||
previewApiUrl?: Maybe<Scalars['String']['output']>;
|
||||
previewToken?: Maybe<Scalars['String']['output']>;
|
||||
previewUrl: Scalars['String']['output'];
|
||||
/** @deprecated Use previewUrl instead. */
|
||||
@@ -13337,6 +13345,8 @@ export type WebPreviewOnlyRunSessionRemoteConfig = {
|
||||
*/
|
||||
egressUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Session token gating the web preview. Null when the preview runs ungated. */
|
||||
/** URL of the preview server, for reaching its API rather than its page. */
|
||||
previewApiUrl?: Maybe<Scalars['String']['output']>;
|
||||
webPreviewToken?: Maybe<Scalars['String']['output']>;
|
||||
webPreviewUrl: Scalars['String']['output'];
|
||||
};
|
||||
@@ -16386,11 +16396,11 @@ export type DeviceRunSessionByIdQueryVariables = Exact<{
|
||||
|
||||
|
||||
export type DeviceRunSessionByIdQuery = { __typename?: 'RootQuery', deviceRunSessions: { __typename?: 'DeviceRunSessionQuery', byId: { __typename?: 'DeviceRunSession', id: string, name?: string | null, tags: Array<string>, status: DeviceRunSessionStatus, type: DeviceRunSessionType, platform: AppPlatform, createdAt: any, startedAt?: any | null, finishedAt?: any | null, updatedAt: any, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, artifacts: Array<{ __typename?: 'DeviceRunSessionArtifact', id: string, name: string, filename: string, downloadUrl: string, fileSizeBytes?: number | null, metadata?: any | null, createdAt: any, updatedAt: any }>, remoteConfig?:
|
||||
| { __typename: 'AgentDeviceRunSessionRemoteConfig', agentDeviceRemoteSessionUrl: string, agentDeviceRemoteSessionToken: string, webPreviewUrl?: string | null, webPreviewToken?: string | null, egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null }
|
||||
| { __typename: 'AppiumRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, appiumUrl: string, capabilities: any, webPreviewUrl?: string | null, webPreviewToken?: string | null }
|
||||
| { __typename: 'ArgentRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, toolsUrl: string, toolsAuthToken?: string | null, webPreviewUrl?: string | null, webPreviewToken?: string | null }
|
||||
| { __typename: 'ServeSimRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, previewUrl: string, previewToken?: string | null }
|
||||
| { __typename: 'WebPreviewOnlyRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, previewUrl: string, previewToken?: string | null }
|
||||
| { __typename: 'AgentDeviceRunSessionRemoteConfig', agentDeviceRemoteSessionUrl: string, agentDeviceRemoteSessionToken: string, webPreviewUrl?: string | null, webPreviewToken?: string | null, previewApiUrl?: string | null, egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null }
|
||||
| { __typename: 'AppiumRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, appiumUrl: string, capabilities: any, webPreviewUrl?: string | null, webPreviewToken?: string | null, previewApiUrl?: string | null }
|
||||
| { __typename: 'ArgentRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, toolsUrl: string, toolsAuthToken?: string | null, webPreviewUrl?: string | null, webPreviewToken?: string | null, previewApiUrl?: string | null }
|
||||
| { __typename: 'ServeSimRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, previewUrl: string, previewToken?: string | null, previewApiUrl?: string | null }
|
||||
| { __typename: 'WebPreviewOnlyRunSessionRemoteConfig', egressUrl?: string | null, egressToken?: string | null, egressFingerprint?: string | null, egressPort?: number | null, previewApiUrl?: string | null, previewUrl: string, previewToken?: string | null }
|
||||
| null, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus } | null } } };
|
||||
|
||||
export type DeviceRunSessionsByAppIdQueryVariables = Exact<{
|
||||
|
||||
@@ -88,6 +88,7 @@ export const DeviceRunSessionQuery = {
|
||||
agentDeviceRemoteSessionToken
|
||||
webPreviewUrl
|
||||
webPreviewToken
|
||||
previewApiUrl
|
||||
egressUrl
|
||||
egressToken
|
||||
egressFingerprint
|
||||
@@ -102,6 +103,7 @@ export const DeviceRunSessionQuery = {
|
||||
toolsAuthToken
|
||||
webPreviewUrl
|
||||
webPreviewToken
|
||||
previewApiUrl
|
||||
}
|
||||
... on AppiumRunSessionRemoteConfig {
|
||||
egressUrl
|
||||
@@ -112,6 +114,7 @@ export const DeviceRunSessionQuery = {
|
||||
capabilities
|
||||
webPreviewUrl
|
||||
webPreviewToken
|
||||
previewApiUrl
|
||||
}
|
||||
... on ServeSimRunSessionRemoteConfig {
|
||||
egressUrl
|
||||
@@ -120,6 +123,7 @@ export const DeviceRunSessionQuery = {
|
||||
egressPort
|
||||
previewUrl
|
||||
previewToken
|
||||
previewApiUrl
|
||||
}
|
||||
... on WebPreviewOnlyRunSessionRemoteConfig {
|
||||
egressUrl
|
||||
@@ -128,6 +132,7 @@ export const DeviceRunSessionQuery = {
|
||||
egressPort
|
||||
previewUrl: webPreviewUrl
|
||||
previewToken: webPreviewToken
|
||||
previewApiUrl
|
||||
}
|
||||
}
|
||||
turtleJobRun {
|
||||
|
||||
@@ -240,25 +240,39 @@ describe('simulator resource class flags', () => {
|
||||
});
|
||||
|
||||
describe(formatPreviewUrl, () => {
|
||||
it('appends the session token for a gated preview', () => {
|
||||
expect(formatPreviewUrl('https://preview.example.test', 'tok-1')).toBe(
|
||||
'https://preview.example.test/?token=tok-1'
|
||||
);
|
||||
const PAGE_URL = 'https://expo.dev/simulator-preview/abc';
|
||||
const SERVER_URL = 'https://web-preview-abc.eas-simulator.ngrok.dev';
|
||||
const pageConfig = {
|
||||
__typename: 'WebPreviewOnlyRunSessionRemoteConfig' as const,
|
||||
previewUrl: PAGE_URL,
|
||||
previewToken: 'tok-1',
|
||||
previewApiUrl: SERVER_URL,
|
||||
};
|
||||
const serverConfig = {
|
||||
__typename: 'ServeSimRunSessionRemoteConfig' as const,
|
||||
previewUrl: SERVER_URL,
|
||||
previewToken: 'tok-1',
|
||||
};
|
||||
|
||||
it('puts the token in the fragment for the expo.dev page, where no request carries it', () => {
|
||||
expect(formatPreviewUrl(pageConfig, PAGE_URL, 'tok-1')).toBe(`${PAGE_URL}#token=tok-1`);
|
||||
});
|
||||
|
||||
it('puts the token on the query for a session that predates the expo.dev page', () => {
|
||||
expect(formatPreviewUrl(serverConfig, SERVER_URL, 'tok-1')).toBe(`${SERVER_URL}/?token=tok-1`);
|
||||
});
|
||||
|
||||
it('leaves the url alone when the preview is ungated', () => {
|
||||
expect(formatPreviewUrl('https://preview.example.test', null)).toBe(
|
||||
'https://preview.example.test'
|
||||
);
|
||||
expect(formatPreviewUrl('https://preview.example.test', undefined)).toBe(
|
||||
'https://preview.example.test'
|
||||
);
|
||||
expect(formatPreviewUrl(pageConfig, PAGE_URL, null)).toBe(PAGE_URL);
|
||||
expect(formatPreviewUrl(pageConfig, PAGE_URL, undefined)).toBe(PAGE_URL);
|
||||
});
|
||||
});
|
||||
|
||||
describe(sanitizeRemoteConfigForJson, () => {
|
||||
const PREVIEW_URL = 'https://preview.example.test';
|
||||
|
||||
const PAGE_URL = 'https://expo.dev/simulator-preview/abc';
|
||||
|
||||
it('moves the token into the preview url and drops the standalone field', () => {
|
||||
const sanitized = sanitizeRemoteConfigForJson({
|
||||
__typename: 'ServeSimRunSessionRemoteConfig' as const,
|
||||
@@ -273,6 +287,22 @@ describe(sanitizeRemoteConfigForJson, () => {
|
||||
expect(JSON.stringify(sanitized)).not.toContain('previewToken');
|
||||
});
|
||||
|
||||
it('puts the token on the api url query, for a caller that cannot send a header', () => {
|
||||
const sanitized = sanitizeRemoteConfigForJson({
|
||||
__typename: 'WebPreviewOnlyRunSessionRemoteConfig' as const,
|
||||
previewUrl: PAGE_URL,
|
||||
previewToken: 'tok-1',
|
||||
previewApiUrl: PREVIEW_URL,
|
||||
});
|
||||
|
||||
expect(sanitized).toEqual({
|
||||
__typename: 'WebPreviewOnlyRunSessionRemoteConfig',
|
||||
previewUrl: `${PAGE_URL}#token=tok-1`,
|
||||
previewApiUrl: `${PREVIEW_URL}/?token=tok-1`,
|
||||
});
|
||||
expect(JSON.stringify(sanitized)).not.toContain('previewToken');
|
||||
});
|
||||
|
||||
it('does the same for a controller session', () => {
|
||||
const sanitized = sanitizeRemoteConfigForJson({
|
||||
__typename: 'ArgentRunSessionRemoteConfig' as const,
|
||||
@@ -368,6 +398,48 @@ describe('gated preview links', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('prints the fragment link for every session the worker gave a preview api url', () => {
|
||||
const PAGE = 'https://expo.dev/simulator-preview/abc';
|
||||
const API = 'https://web-preview-abc.eas-simulator.ngrok.dev';
|
||||
const sessions = [
|
||||
{
|
||||
__typename: 'ServeSimRunSessionRemoteConfig' as const,
|
||||
previewUrl: PAGE,
|
||||
previewToken: 'tok-1',
|
||||
previewApiUrl: API,
|
||||
},
|
||||
{
|
||||
__typename: 'WebPreviewOnlyRunSessionRemoteConfig' as const,
|
||||
previewUrl: PAGE,
|
||||
previewToken: 'tok-1',
|
||||
previewApiUrl: API,
|
||||
},
|
||||
{
|
||||
__typename: 'AgentDeviceRunSessionRemoteConfig' as const,
|
||||
agentDeviceRemoteSessionUrl: 'https://daemon.example.test',
|
||||
agentDeviceRemoteSessionToken: 'daemon-token',
|
||||
webPreviewUrl: PAGE,
|
||||
webPreviewToken: 'tok-1',
|
||||
previewApiUrl: API,
|
||||
},
|
||||
{
|
||||
__typename: 'ArgentRunSessionRemoteConfig' as const,
|
||||
toolsUrl: 'https://argent.example.test',
|
||||
toolsAuthToken: 'argent-token',
|
||||
webPreviewUrl: PAGE,
|
||||
webPreviewToken: 'tok-1',
|
||||
previewApiUrl: API,
|
||||
},
|
||||
{ ...iosAppiumConfig, webPreviewUrl: PAGE, webPreviewToken: 'tok-1', previewApiUrl: API },
|
||||
];
|
||||
|
||||
for (const remoteConfig of sessions) {
|
||||
const instructions = formatRemoteSessionInstructions(remoteConfig, 'env');
|
||||
expect(instructions).toContain(`${PAGE}#token=tok-1`);
|
||||
expect(instructions).not.toContain('?token=');
|
||||
}
|
||||
});
|
||||
|
||||
it('prints the plain url when the preview is ungated', () => {
|
||||
const instructions = formatRemoteSessionInstructions(
|
||||
{ __typename: 'ServeSimRunSessionRemoteConfig' as const, previewUrl: PREVIEW },
|
||||
|
||||
@@ -221,11 +221,29 @@ function getControllerEnvironmentVariables(
|
||||
|
||||
type RemoteSessionInstructionsConfigType = 'env' | 'dotenv';
|
||||
|
||||
function previewUrlIsWebsitePage(remoteConfig: DeviceRunSessionRemoteConfig): boolean {
|
||||
return remoteConfig.previewApiUrl != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview link for a session. A gated serve-sim needs the session token, and a browser cannot send
|
||||
* a header on a page load, so it rides the query. serve-sim swaps it for a cookie on the first load.
|
||||
* Preview link for a session. A gated preview needs the session token, and a browser cannot send
|
||||
* a header on a page load. The expo.dev preview page reads it from the fragment, which no request
|
||||
* carries; a preview server reads it from the query and swaps it for a cookie on the first load.
|
||||
*/
|
||||
export function formatPreviewUrl(url: string, token: string | null | undefined): string {
|
||||
export function formatPreviewUrl(
|
||||
remoteConfig: DeviceRunSessionRemoteConfig,
|
||||
url: string,
|
||||
token: string | null | undefined
|
||||
): string {
|
||||
if (!token || !previewUrlIsWebsitePage(remoteConfig)) {
|
||||
return formatPreviewApiUrl(url, token);
|
||||
}
|
||||
const withToken = new URL(url);
|
||||
withToken.hash = new URLSearchParams({ token }).toString();
|
||||
return withToken.toString();
|
||||
}
|
||||
|
||||
function formatPreviewApiUrl(url: string, token: string | null | undefined): string {
|
||||
if (!token) {
|
||||
return url;
|
||||
}
|
||||
@@ -235,9 +253,9 @@ export function formatPreviewUrl(url: string, token: string | null | undefined):
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote config for `--json`. The preview URL carries the token and the standalone token field is
|
||||
* dropped, so a consumer gets one URL that works rather than a bare URL that 401s next to a secret
|
||||
* it has to know to combine.
|
||||
* Remote config for `--json`. Both URLs carry the token and the standalone token field is dropped,
|
||||
* so a consumer gets URLs that work rather than bare URLs that 401 next to a secret it has to know
|
||||
* to combine.
|
||||
*/
|
||||
export function sanitizeRemoteConfigForJson(
|
||||
remoteConfig: DeviceRunSessionRemoteConfig
|
||||
@@ -246,7 +264,13 @@ export function sanitizeRemoteConfigForJson(
|
||||
case 'ServeSimRunSessionRemoteConfig':
|
||||
case 'WebPreviewOnlyRunSessionRemoteConfig': {
|
||||
const { previewToken, ...rest } = remoteConfig;
|
||||
return { ...rest, previewUrl: formatPreviewUrl(remoteConfig.previewUrl, previewToken) };
|
||||
return {
|
||||
...rest,
|
||||
previewUrl: formatPreviewUrl(remoteConfig, remoteConfig.previewUrl, previewToken),
|
||||
previewApiUrl: remoteConfig.previewApiUrl
|
||||
? formatPreviewApiUrl(remoteConfig.previewApiUrl, previewToken)
|
||||
: remoteConfig.previewApiUrl,
|
||||
};
|
||||
}
|
||||
case 'AgentDeviceRunSessionRemoteConfig':
|
||||
case 'ArgentRunSessionRemoteConfig':
|
||||
@@ -255,8 +279,11 @@ export function sanitizeRemoteConfigForJson(
|
||||
return {
|
||||
...rest,
|
||||
webPreviewUrl: remoteConfig.webPreviewUrl
|
||||
? formatPreviewUrl(remoteConfig.webPreviewUrl, webPreviewToken)
|
||||
? formatPreviewUrl(remoteConfig, remoteConfig.webPreviewUrl, webPreviewToken)
|
||||
: remoteConfig.webPreviewUrl,
|
||||
previewApiUrl: remoteConfig.previewApiUrl
|
||||
? formatPreviewApiUrl(remoteConfig.previewApiUrl, webPreviewToken)
|
||||
: remoteConfig.previewApiUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -331,7 +358,7 @@ function formatControllerInstructions(
|
||||
'',
|
||||
'🌐 Open the following URL in your browser to preview the simulator:',
|
||||
'',
|
||||
formatPreviewUrl(remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
formatPreviewUrl(remoteConfig, remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
@@ -369,7 +396,7 @@ function formatControllerInstructions(
|
||||
'',
|
||||
'🌐 Open the following URL in your browser to preview the simulator:',
|
||||
'',
|
||||
formatPreviewUrl(remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
formatPreviewUrl(remoteConfig, remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
@@ -397,7 +424,7 @@ function formatControllerInstructions(
|
||||
'',
|
||||
'Open the simulator preview:',
|
||||
'',
|
||||
formatPreviewUrl(remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
formatPreviewUrl(remoteConfig, remoteConfig.webPreviewUrl, remoteConfig.webPreviewToken)
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
@@ -406,13 +433,13 @@ function formatControllerInstructions(
|
||||
return [
|
||||
'🌐 Open the following URL in your browser to access the simulator:',
|
||||
'',
|
||||
formatPreviewUrl(remoteConfig.previewUrl, remoteConfig.previewToken),
|
||||
formatPreviewUrl(remoteConfig, remoteConfig.previewUrl, remoteConfig.previewToken),
|
||||
].join('\n');
|
||||
case 'WebPreviewOnlyRunSessionRemoteConfig':
|
||||
return [
|
||||
'🌐 Open the following URL in your browser to access the simulator:',
|
||||
'',
|
||||
formatPreviewUrl(remoteConfig.previewUrl, remoteConfig.previewToken),
|
||||
formatPreviewUrl(remoteConfig, remoteConfig.previewUrl, remoteConfig.previewToken),
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "24.5.0",
|
||||
"version": "24.7.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": "24.5.0",
|
||||
"@expo/build-tools": "24.7.0",
|
||||
"@expo/bunyan": "^4.0.1",
|
||||
"@expo/eas-build-job": "24.5.0",
|
||||
"@expo/logger": "24.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "24.5.0",
|
||||
"version": "24.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "24.5.0",
|
||||
"@expo/build-tools": "24.7.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/eas-build-job": "24.5.0",
|
||||
|
||||
@@ -2291,7 +2291,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:24.5.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:24.7.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -2972,7 +2972,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:24.5.0"
|
||||
"@expo/build-tools": "npm:24.7.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/eas-build-job": "npm:24.5.0"
|
||||
@@ -10415,7 +10415,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:24.5.0"
|
||||
"@expo/build-tools": "npm:24.7.0"
|
||||
"@expo/bunyan": "npm:^4.0.1"
|
||||
"@expo/eas-build-job": "npm:24.5.0"
|
||||
"@expo/logger": "npm:24.0.0"
|
||||
|
||||
Reference in new issue
Block a user