mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
329ddccd61 | ||
|
|
1db7281e8b | ||
|
|
1e8a0acabe | ||
|
|
6bfb8af57f | ||
|
|
231f29ecf1 |
No files matched your search
+2
-3
@@ -8,19 +8,18 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add screenshots and previews support to `metadata:push` and `metadata:pull`. ([#3301](https://github.com/expo/eas-cli/pull/3301) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Auto-generate `ios.bundleIdentifier` and `android.package` in non-interactive mode when not set in app config. ([#3399](https://github.com/expo/eas-cli/pull/3399) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
- [eas-cli] Add `--non-interactive` flag to `metadata:push` and `metadata:pull` commands with ASC API Key auth support. ([#3548](https://github.com/expo/eas-cli/pull/3548) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Fix workflow:logs for builds using built-in EAS build steps. ([#3523](https://github.com/expo/eas-cli/pull/3523) by [@douglowder](https://github.com/douglowder))
|
||||
- [build-tools][worker] Read Expo app config with `expo config` CLI invocation before falling back to `@expo/config` ([#3536](https://github.com/expo/eas-cli/pull/3536) by [@kitten](https://github.com/kitten))
|
||||
- Fix `hasIgnoredIosProjectAsync()` always returning `false` for ignored iOS projects. ([#3562](https://github.com/expo/eas-cli/pull/3562) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Bump apple-utils. ([#3545](https://github.com/expo/eas-cli/pull/3545) by [@quinlanj](https://github.com/quinlanj))
|
||||
- [eas-cli][build-tools][worker] Bump `@expo/config` and `@expo/config-plugins`. ([#3527](https://github.com/expo/eas-cli/pull/3527) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [18.4.0](https://github.com/expo/eas-cli/releases/tag/v18.4.0) - 2026-03-16
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -35,19 +35,19 @@
|
||||
"test": "yarn jest-unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/downloader": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
"@google-cloud/storage": "^7.11.2",
|
||||
"@urql/core": "^6.0.1",
|
||||
|
||||
@@ -19,5 +19,12 @@ export { PackageManager } from './utils/packageManager';
|
||||
export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
|
||||
|
||||
export { Hook, runHookIfPresent } from './utils/hooks';
|
||||
export {
|
||||
maybeStartPreparingPrecompiledModules,
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
shouldUsePrecompiledDependencies,
|
||||
startPreparingPrecompiledDependencies,
|
||||
waitForPrecompiledModulesPreparationAsync,
|
||||
} from './utils/precompiledModules';
|
||||
|
||||
export * from './generic';
|
||||
@@ -0,0 +1,127 @@
|
||||
import downloadFile from '@expo/downloader';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { mkdirp, mkdtemp, move, remove } from 'fs-extra';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
|
||||
import { createTestIosJob } from '../../__tests__/utils/job';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { BuildContext } from '../../context';
|
||||
import {
|
||||
startPreparingPrecompiledDependencies,
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
} from '../../utils/precompiledModules';
|
||||
import { installPods } from '../pod';
|
||||
|
||||
jest.mock('@expo/downloader');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('@expo/turtle-spawn');
|
||||
jest.mock('node-stream-zip', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
async: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe(installPods.name, () => {
|
||||
const extract = jest.fn();
|
||||
const close = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(remove).mockImplementation(async () => undefined as any);
|
||||
jest.mocked(mkdirp).mockImplementation(async () => undefined as any);
|
||||
jest.mocked(move).mockImplementation(async () => undefined as any);
|
||||
let mkdtempCallCount = 0;
|
||||
jest.mocked(mkdtemp).mockImplementation(async () => {
|
||||
mkdtempCallCount += 1;
|
||||
return mkdtempCallCount === 1
|
||||
? ('/tmp/precompiled-modules-archive' as any)
|
||||
: ('/tmp/precompiled-modules-staging' as any);
|
||||
});
|
||||
extract.mockReset().mockResolvedValue(undefined);
|
||||
close.mockReset().mockResolvedValue(undefined);
|
||||
jest.mocked(StreamZip.async).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
extract,
|
||||
close,
|
||||
}) as any
|
||||
);
|
||||
});
|
||||
|
||||
it('waits for precompiled dependencies before running pod install', async () => {
|
||||
let resolvePreparation: (() => void) | undefined;
|
||||
const preparationPromise = new Promise<void>(resolve => {
|
||||
resolvePreparation = resolve;
|
||||
});
|
||||
jest.mocked(downloadFile).mockReturnValue(preparationPromise);
|
||||
const ctx = new BuildContext(
|
||||
createTestIosJob({
|
||||
buildCredentials: undefined,
|
||||
}),
|
||||
{
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
startPreparingPrecompiledDependencies(ctx, [
|
||||
'https://example.com/precompiled-modules-0.zip',
|
||||
'https://example.com/precompiled-modules-1.zip',
|
||||
]);
|
||||
|
||||
jest.mocked(spawn).mockResolvedValue({} as any);
|
||||
|
||||
const installPodsPromise = installPods(ctx, {});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
|
||||
resolvePreparation?.();
|
||||
await installPodsPromise;
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pod',
|
||||
['install'],
|
||||
expect.objectContaining({
|
||||
cwd: '/workingdir/build/ios',
|
||||
})
|
||||
);
|
||||
expect(mkdirp).toHaveBeenCalledWith(PRECOMPILED_MODULES_PATH);
|
||||
});
|
||||
|
||||
it('continues with pod install when precompiled dependencies preparation fails', async () => {
|
||||
jest.mocked(downloadFile).mockRejectedValue(new Error('download failed'));
|
||||
const ctx = new BuildContext(
|
||||
createTestIosJob({
|
||||
buildCredentials: undefined,
|
||||
}),
|
||||
{
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
startPreparingPrecompiledDependencies(ctx, ['https://example.com/precompiled-modules-0.zip']);
|
||||
|
||||
jest.mocked(spawn).mockResolvedValue({} as any);
|
||||
|
||||
await expect(installPods(ctx, {})).resolves.toEqual({
|
||||
spawnPromise: expect.any(Promise),
|
||||
});
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pod',
|
||||
['install'],
|
||||
expect.objectContaining({
|
||||
cwd: '/workingdir/build/ios',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,21 @@ import spawn, { SpawnOptions, SpawnPromise, SpawnResult } from '@expo/turtle-spa
|
||||
import path from 'path';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { waitForPrecompiledModulesPreparationAsync } from '../utils/precompiledModules';
|
||||
|
||||
export async function installPods<TJob extends Ios.Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
{ infoCallbackFn }: SpawnOptions
|
||||
): Promise<{ spawnPromise: SpawnPromise<SpawnResult> }> {
|
||||
try {
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
} catch (err) {
|
||||
ctx.logger.warn(
|
||||
{ err },
|
||||
'Precompiled dependencies were not prepared successfully, continuing with pod install'
|
||||
);
|
||||
}
|
||||
|
||||
const iosDir = path.join(ctx.getReactNativeProjectDirectory(), 'ios');
|
||||
|
||||
const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? ['--verbose'] : [];
|
||||
|
||||
@@ -17,6 +17,7 @@ import { injectAndroidCredentialsFunction } from './functions/injectAndroidCrede
|
||||
import { createInstallMaestroBuildFunction } from './functions/installMaestro';
|
||||
import { createInstallNodeModulesBuildFunction } from './functions/installNodeModules';
|
||||
import { createInstallPodsBuildFunction } from './functions/installPods';
|
||||
import { createInternalEasMaestroTestFunction } from './functions/internalMaestroTest';
|
||||
import { createPrebuildBuildFunction } from './functions/prebuild';
|
||||
import { createReadIpaInfoBuildFunction } from './functions/readIpaInfo';
|
||||
import { createRepackBuildFunction } from './functions/repack';
|
||||
@@ -80,6 +81,8 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
createSubmissionEntityFunction(),
|
||||
createUploadToAscBuildFunction(),
|
||||
|
||||
createInternalEasMaestroTestFunction(ctx),
|
||||
|
||||
createReportMaestroTestResultsFunction(ctx),
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { waitForPrecompiledModulesPreparationAsync } from '../../../utils/precompiledModules';
|
||||
import { createInstallPodsBuildFunction } from '../installPods';
|
||||
|
||||
jest.mock('@expo/turtle-spawn');
|
||||
jest.mock('../../../utils/precompiledModules', () => ({
|
||||
...jest.requireActual('../../../utils/precompiledModules'),
|
||||
waitForPrecompiledModulesPreparationAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(createInstallPodsBuildFunction, () => {
|
||||
it('waits for precompiled dependencies before running pod install', async () => {
|
||||
jest.mocked(spawn).mockResolvedValue({} as any);
|
||||
jest.mocked(waitForPrecompiledModulesPreparationAsync).mockResolvedValue(undefined);
|
||||
|
||||
const installPods = createInstallPodsBuildFunction();
|
||||
const globalContext = createGlobalContextMock({});
|
||||
const buildStep = installPods.createBuildStepFromFunctionCall(globalContext, {
|
||||
callInputs: {},
|
||||
});
|
||||
|
||||
await buildStep.executeAsync();
|
||||
|
||||
expect(waitForPrecompiledModulesPreparationAsync).toHaveBeenCalled();
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pod',
|
||||
['install'],
|
||||
expect.objectContaining({
|
||||
cwd: globalContext.defaultWorkingDirectory,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('continues with pod install when precompiled dependencies preparation fails', async () => {
|
||||
jest.mocked(spawn).mockResolvedValue({} as any);
|
||||
jest
|
||||
.mocked(waitForPrecompiledModulesPreparationAsync)
|
||||
.mockRejectedValue(new Error('precompiled dependencies failed'));
|
||||
|
||||
const installPods = createInstallPodsBuildFunction();
|
||||
const globalContext = createGlobalContextMock({});
|
||||
const buildStep = installPods.createBuildStepFromFunctionCall(globalContext, {
|
||||
callInputs: {},
|
||||
});
|
||||
|
||||
await expect(buildStep.executeAsync()).resolves.toBeUndefined();
|
||||
|
||||
expect(waitForPrecompiledModulesPreparationAsync).toHaveBeenCalled();
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pod',
|
||||
['install'],
|
||||
expect.objectContaining({
|
||||
cwd: globalContext.defaultWorkingDirectory,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
import { spawnAsync } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { AndroidEmulatorUtils } from '../../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { retryAsync } from '../../../utils/retry';
|
||||
import { createInternalEasMaestroTestFunction } from '../internalMaestroTest';
|
||||
|
||||
jest.mock('@expo/steps', () => ({
|
||||
...jest.requireActual('@expo/steps'),
|
||||
spawnAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/IosSimulatorUtils');
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils');
|
||||
jest.mock('../../../utils/findMaestroPathsFlowsToExecuteAsync');
|
||||
jest.mock('../../../utils/retry', () => ({
|
||||
retryAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedSpawnAsync = jest.mocked(spawnAsync);
|
||||
const mockedSpawn = jest.mocked(spawn);
|
||||
const mockedRetryAsync = jest.mocked(retryAsync);
|
||||
const mockedAndroidUtils = jest.mocked(AndroidEmulatorUtils);
|
||||
const mockedIosUtils = jest.mocked(IosSimulatorUtils);
|
||||
const mockedFindFlows = jest.mocked(findMaestroPathsFlowsToExecuteAsync);
|
||||
|
||||
describe(createInternalEasMaestroTestFunction, () => {
|
||||
const mockUploadArtifact = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vol.mkdirSync(os.tmpdir(), { recursive: true });
|
||||
|
||||
mockedSpawnAsync.mockResolvedValue(undefined as any);
|
||||
mockedSpawn.mockResolvedValue({ stdout: 'source-emulator\n', stderr: '' } as any);
|
||||
mockUploadArtifact.mockResolvedValue({ artifactId: null });
|
||||
|
||||
mockedRetryAsync.mockImplementation(async (fn, { retryOptions }) => {
|
||||
let lastErr: unknown;
|
||||
for (let attemptCount = 0; attemptCount <= retryOptions.retries; attemptCount++) {
|
||||
try {
|
||||
return await fn(attemptCount);
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
});
|
||||
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([
|
||||
{ name: 'iPhone 15', udid: 'test-udid-123' } as any,
|
||||
]);
|
||||
mockedIosUtils.cloneAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.startAsync.mockResolvedValue({ udid: 'cloned-udid' } as any);
|
||||
mockedIosUtils.waitForReadyAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedIosUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedAndroidUtils.getAttachedDevicesAsync.mockResolvedValue([
|
||||
{ serialId: 'emulator-5554', state: 'device' } as any,
|
||||
]);
|
||||
mockedAndroidUtils.cloneAsync.mockResolvedValue(undefined as any);
|
||||
mockedAndroidUtils.startAsync.mockResolvedValue({
|
||||
serialId: 'emulator-5556',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync.mockResolvedValue(undefined as any);
|
||||
mockedAndroidUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedAndroidUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedFindFlows.mockResolvedValue(['/project/.maestro/home.yml']);
|
||||
});
|
||||
|
||||
function createStep(overrides?: { callInputs?: Record<string, unknown> }) {
|
||||
const ctx = {
|
||||
runtimeApi: { uploadArtifact: mockUploadArtifact },
|
||||
};
|
||||
const fn = createInternalEasMaestroTestFunction(ctx as any);
|
||||
return fn.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
}),
|
||||
{
|
||||
callInputs: {
|
||||
platform: 'ios',
|
||||
flow_paths: JSON.stringify(['.maestro']),
|
||||
...overrides?.callInputs,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('sets junit_report_directory output when output_format is junit', async () => {
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toMatch(/maestro-reports-/);
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory output when output_format is not junit', async () => {
|
||||
const step = createStep();
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory when function throws before reaching output code', async () => {
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([]);
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
|
||||
try {
|
||||
await step.executeAsync();
|
||||
} catch {
|
||||
// Expected - no booted device found
|
||||
}
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
|
||||
it('retries Android clone startup with increasing timeouts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-1',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-2',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync
|
||||
.mockRejectedValueOnce(new Error('network unavailable'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { platform: 'android' },
|
||||
});
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-1',
|
||||
timeoutMs: 60_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-2',
|
||||
timeoutMs: 120_000,
|
||||
})
|
||||
);
|
||||
expect(mockedAndroidUtils.deleteAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-1',
|
||||
deviceName: 'eas-simulator-0-0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('generates per-attempt report filenames when retrying', async () => {
|
||||
// Mock sequence:
|
||||
// 1. xcrun simctl shutdown → succeeds (beforeEach default is fine)
|
||||
// 2. maestro test attempt 0 → fails (triggers retry)
|
||||
// 3. maestro test attempt 1 → succeeds (beforeEach default)
|
||||
mockedSpawnAsync
|
||||
.mockResolvedValueOnce(undefined as any) // xcrun simctl shutdown
|
||||
.mockRejectedValueOnce(new Error('Maestro test failed')); // maestro attempt 0
|
||||
// Attempt 1 falls through to beforeEach's mockResolvedValue default
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit', retries: 2 },
|
||||
});
|
||||
await step.executeAsync();
|
||||
|
||||
// Filter to just the maestro test command calls (skip xcrun simctl shutdown)
|
||||
const maestroCalls = mockedSpawnAsync.mock.calls.filter(([cmd]) => cmd === 'maestro');
|
||||
expect(maestroCalls).toHaveLength(2);
|
||||
|
||||
// Extract --output paths from maestro calls
|
||||
const outputArgs = maestroCalls.map(([, args]) => args[args.indexOf('--output') + 1]);
|
||||
|
||||
expect(outputArgs[0]).toContain('attempt-0');
|
||||
expect(outputArgs[1]).toContain('attempt-1');
|
||||
// Filenames must be unique (not overwriting)
|
||||
expect(outputArgs[0]).not.toBe(outputArgs[1]);
|
||||
});
|
||||
|
||||
it('fails Android flow when clone startup exhausts all attempts', async () => {
|
||||
mockedAndroidUtils.startAsync
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-1',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-2',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
serialId: 'emulator-attempt-3',
|
||||
emulatorPromise: Promise.resolve({}),
|
||||
} as any);
|
||||
mockedAndroidUtils.waitForReadyAsync.mockRejectedValue(new Error('network unavailable'));
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { platform: 'android' },
|
||||
});
|
||||
|
||||
await expect(step.executeAsync()).rejects.toThrow('network unavailable');
|
||||
|
||||
expect(mockedAndroidUtils.waitForReadyAsync).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
serialId: 'emulator-attempt-3',
|
||||
timeoutMs: 180_000,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BuildFunction } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { waitForPrecompiledModulesPreparationAsync } from '../../utils/precompiledModules';
|
||||
|
||||
export function createInstallPodsBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
@@ -8,6 +10,14 @@ export function createInstallPodsBuildFunction(): BuildFunction {
|
||||
name: 'Install Pods',
|
||||
__metricsId: 'eas/install_pods',
|
||||
fn: async (stepsCtx, { env }) => {
|
||||
try {
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
} catch (err) {
|
||||
stepsCtx.logger.warn(
|
||||
{ err },
|
||||
'Precompiled dependencies were not prepared successfully, continuing with pod install'
|
||||
);
|
||||
}
|
||||
stepsCtx.logger.info('Installing pods');
|
||||
const verboseFlag = stepsCtx.global.env['EAS_VERBOSE'] === '1' ? ['--verbose'] : [];
|
||||
const cocoapodsDeploymentFlag =
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
import { GenericArtifactType } from '@expo/eas-build-job';
|
||||
import { PipeMode, bunyan } from '@expo/logger';
|
||||
import { Result, asyncResult, result } from '@expo/results';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepEnv,
|
||||
BuildStepInput,
|
||||
BuildStepInputValueTypeName,
|
||||
BuildStepOutput,
|
||||
spawnAsync,
|
||||
} from '@expo/steps';
|
||||
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import {
|
||||
AndroidDeviceSerialId,
|
||||
AndroidEmulatorUtils,
|
||||
AndroidVirtualDeviceName,
|
||||
} from '../../utils/AndroidEmulatorUtils';
|
||||
import { IosSimulatorName, IosSimulatorUtils } from '../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { retryAsync } from '../../utils/retry';
|
||||
import { PlatformToProperNounMap } from '../../utils/strings';
|
||||
|
||||
export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: '__maestro_test',
|
||||
__metricsId: 'eas/__maestro_test',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
id: 'platform',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.JSON,
|
||||
id: 'flow_paths',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.NUMBER,
|
||||
id: 'retries',
|
||||
defaultValue: 1,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
id: 'include_tags',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
id: 'exclude_tags',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.NUMBER,
|
||||
id: 'shards',
|
||||
defaultValue: 1,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
id: 'output_format',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
|
||||
id: 'record_screen',
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputProviders: [
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'test_reports_artifact_id',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'junit_report_directory',
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
fn: async (stepCtx, { inputs: _inputs, env, outputs }) => {
|
||||
// inputs come in form of { value: unknown }. Here we parse them into a typed and validated object.
|
||||
const {
|
||||
platform,
|
||||
flow_paths,
|
||||
retries,
|
||||
include_tags,
|
||||
exclude_tags,
|
||||
shards,
|
||||
output_format,
|
||||
record_screen,
|
||||
} = z
|
||||
.object({
|
||||
platform: z.enum(['ios', 'android']),
|
||||
flow_paths: z.array(z.string()),
|
||||
retries: z.number().default(1),
|
||||
include_tags: z.string().optional(),
|
||||
exclude_tags: z.string().optional(),
|
||||
shards: z.number().default(1),
|
||||
output_format: z.string().optional(),
|
||||
record_screen: z.boolean().default(false),
|
||||
})
|
||||
.parse(
|
||||
Object.fromEntries(Object.entries(_inputs).map(([key, value]) => [key, value.value]))
|
||||
);
|
||||
|
||||
const flowPathsToExecute: string[] = [];
|
||||
for (const flowPath of flow_paths) {
|
||||
const flowPaths = await findMaestroPathsFlowsToExecuteAsync({
|
||||
workingDirectory: stepCtx.workingDirectory,
|
||||
flowPath,
|
||||
logger: stepCtx.logger,
|
||||
includeTags: include_tags ? include_tags.split(',') : undefined,
|
||||
excludeTags: exclude_tags ? exclude_tags.split(',') : undefined,
|
||||
});
|
||||
if (flowPaths.length === 0) {
|
||||
stepCtx.logger.warn(`No flows to execute found in "${flowPath}".`);
|
||||
continue;
|
||||
}
|
||||
stepCtx.logger.info(
|
||||
`Marking for execution:\n- ${flowPaths
|
||||
.map(flowPath => path.relative(stepCtx.workingDirectory, flowPath))
|
||||
.join('\n- ')}`
|
||||
);
|
||||
stepCtx.logger.info('');
|
||||
flowPathsToExecute.push(...flowPaths);
|
||||
}
|
||||
|
||||
// TODO: Add support for shards. (Shouldn't be too difficult.)
|
||||
if (shards > 1) {
|
||||
stepCtx.logger.warn(
|
||||
'Sharding support has been temporarily disabled. Running tests on a single shard.'
|
||||
);
|
||||
}
|
||||
|
||||
// eas/__maestro_test does not start devices, it expects a single device to be already running
|
||||
// and configured with the app. Here we find the booted device and stop it.
|
||||
|
||||
let sourceDeviceIdentifier: IosSimulatorName | AndroidVirtualDeviceName;
|
||||
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
const bootedDevices = await IosSimulatorUtils.getAvailableDevicesAsync({
|
||||
env,
|
||||
filter: 'booted',
|
||||
});
|
||||
if (bootedDevices.length === 0) {
|
||||
throw new Error('No booted iOS Simulator found.');
|
||||
} else if (bootedDevices.length > 1) {
|
||||
throw new Error('Multiple booted iOS Simulators found.');
|
||||
}
|
||||
|
||||
const device = bootedDevices[0];
|
||||
stepCtx.logger.info(`Running tests on iOS Simulator: ${device.name}.`);
|
||||
|
||||
stepCtx.logger.info(`Preparing Simulator for tests...`);
|
||||
await spawnAsync('xcrun', ['simctl', 'shutdown', device.udid], {
|
||||
logger: stepCtx.logger,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
sourceDeviceIdentifier = device.name;
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
const connectedDevices = await AndroidEmulatorUtils.getAttachedDevicesAsync({ env });
|
||||
if (connectedDevices.length === 0) {
|
||||
throw new Error('No booted Android Emulator found.');
|
||||
} else if (connectedDevices.length > 1) {
|
||||
throw new Error('Multiple booted Android Emulators found.');
|
||||
}
|
||||
|
||||
const { serialId } = connectedDevices[0];
|
||||
const adbEmuAvdNameResult = await spawn('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
||||
mode: PipeMode.COMBINED,
|
||||
env,
|
||||
});
|
||||
const avdName = adbEmuAvdNameResult.stdout
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')[0] as AndroidVirtualDeviceName;
|
||||
stepCtx.logger.info(`Running tests on Android Emulator: ${avdName}.`);
|
||||
|
||||
stepCtx.logger.info(`Preparing Emulator for tests...`);
|
||||
await spawnAsync('adb', ['-s', serialId, 'emu', 'kill'], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
// Waiting for emulator to get killed, see ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL.
|
||||
await setTimeout(1000);
|
||||
|
||||
sourceDeviceIdentifier = avdName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// During tests we generate reports and device logs. We store them in temporary directories
|
||||
// and upload them once all tests are done. When a test is retried, new reports overwrite
|
||||
// the old ones. The files are named "flow-${index}" for easier identification.
|
||||
const maestroReportsDir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'maestro-reports-')
|
||||
);
|
||||
const deviceLogsDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'device-logs-'));
|
||||
|
||||
const failedFlows: string[] = [];
|
||||
|
||||
for (const [flowIndex, flowPath] of flowPathsToExecute.entries()) {
|
||||
stepCtx.logger.info('');
|
||||
|
||||
for (let attemptCount = 0; attemptCount < retries; attemptCount++) {
|
||||
// Generate unique report path per attempt (not overwritten on retry)
|
||||
const outputPath = path.join(
|
||||
maestroReportsDir,
|
||||
[
|
||||
`${output_format ? output_format + '-' : ''}report-flow-${flowIndex + 1}-attempt-${attemptCount}`,
|
||||
MaestroOutputFormatToExtensionMap[output_format ?? 'noop'],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('.')
|
||||
);
|
||||
const localDeviceName = `eas-simulator-${flowIndex}-${attemptCount}` as
|
||||
| IosSimulatorName
|
||||
| AndroidVirtualDeviceName;
|
||||
|
||||
// If the test passes, but the recording fails, we don't want to make the test fail,
|
||||
// so we return two separate results.
|
||||
const {
|
||||
fnResult: { fnResult, recordingResult },
|
||||
logsResult,
|
||||
} = await withCleanDeviceAsync({
|
||||
platform,
|
||||
sourceDeviceIdentifier,
|
||||
localDeviceName,
|
||||
env,
|
||||
logger: stepCtx.logger,
|
||||
fn: async ({ deviceIdentifier }) => {
|
||||
return await maybeWithScreenRecordingAsync({
|
||||
shouldRecord: record_screen,
|
||||
platform,
|
||||
deviceIdentifier,
|
||||
env,
|
||||
logger: stepCtx.logger,
|
||||
fn: async () => {
|
||||
stepCtx.logger.info('');
|
||||
|
||||
const [command, ...args] = getMaestroTestCommand({
|
||||
flow_path: flowPath,
|
||||
output_format,
|
||||
output_path: outputPath,
|
||||
});
|
||||
|
||||
try {
|
||||
await spawnAsync(command, args, {
|
||||
logger: stepCtx.logger,
|
||||
cwd: stepCtx.workingDirectory,
|
||||
env,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
} finally {
|
||||
stepCtx.logger.info('');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Move device logs to the device logs directory.
|
||||
if (logsResult?.ok) {
|
||||
try {
|
||||
const extension = path.extname(logsResult.value.outputPath);
|
||||
const destinationPath = path.join(
|
||||
deviceLogsDir,
|
||||
`flow-${flowIndex}-attempt-${attemptCount}${extension}`
|
||||
);
|
||||
|
||||
await fs.promises.rename(logsResult.value.outputPath, destinationPath);
|
||||
} catch (err) {
|
||||
stepCtx.logger.warn({ err }, 'Failed to prepare device logs for upload.');
|
||||
}
|
||||
} else if (logsResult?.reason) {
|
||||
stepCtx.logger.error({ err: logsResult.reason }, 'Failed to collect device logs.');
|
||||
}
|
||||
|
||||
const isLastAttempt = fnResult.ok || attemptCount === retries - 1;
|
||||
if (isLastAttempt && recordingResult.value) {
|
||||
try {
|
||||
await ctx.runtimeApi.uploadArtifact({
|
||||
logger: stepCtx.logger,
|
||||
artifact: {
|
||||
// TODO(sjchmiela): Add metadata to artifacts so we don't need to encode flow path and attempt in the name.
|
||||
name: `Screen Recording (${flowIndex}-${path.basename(
|
||||
flowPath,
|
||||
path.extname(flowPath)
|
||||
)})`,
|
||||
paths: [recordingResult.value],
|
||||
type: GenericArtifactType.OTHER,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
stepCtx.logger.warn({ err }, 'Failed to upload screen recording.');
|
||||
}
|
||||
}
|
||||
|
||||
if (fnResult.ok) {
|
||||
stepCtx.logger.info(`Test passed.`);
|
||||
// Break out of the retry loop.
|
||||
break;
|
||||
}
|
||||
|
||||
if (attemptCount < retries - 1) {
|
||||
stepCtx.logger.info(`Retrying test...`);
|
||||
stepCtx.logger.info('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// fnResult.reason is not super interesting, but it does print out the full command so we can keep it for debugging purposes.
|
||||
stepCtx.logger.error({ err: fnResult.reason }, 'Test errored.');
|
||||
failedFlows.push(flowPath);
|
||||
}
|
||||
}
|
||||
|
||||
stepCtx.logger.info('');
|
||||
|
||||
// When all tests are done, we upload the reports and device logs.
|
||||
const generatedMaestroReports = await fs.promises.readdir(maestroReportsDir);
|
||||
if (generatedMaestroReports.length === 0) {
|
||||
stepCtx.logger.warn('No reports were generated.');
|
||||
} else {
|
||||
stepCtx.logger.info(`Uploading reports...`);
|
||||
try {
|
||||
const { artifactId } = await ctx.runtimeApi.uploadArtifact({
|
||||
logger: stepCtx.logger,
|
||||
artifact: {
|
||||
name: `${PlatformToProperNounMap[platform]} Maestro Test Reports (${output_format})`,
|
||||
paths: [maestroReportsDir],
|
||||
type: GenericArtifactType.OTHER,
|
||||
},
|
||||
});
|
||||
if (artifactId) {
|
||||
outputs.test_reports_artifact_id.set(artifactId);
|
||||
}
|
||||
} catch (err) {
|
||||
stepCtx.logger.error({ err }, 'Failed to upload reports.');
|
||||
}
|
||||
}
|
||||
|
||||
if (output_format === 'junit') {
|
||||
outputs.junit_report_directory.set(maestroReportsDir);
|
||||
}
|
||||
|
||||
const generatedDeviceLogs = await fs.promises.readdir(deviceLogsDir);
|
||||
if (generatedDeviceLogs.length === 0) {
|
||||
stepCtx.logger.warn('No device logs were successfully collected.');
|
||||
} else {
|
||||
stepCtx.logger.info(`Uploading device logs...`);
|
||||
try {
|
||||
await ctx.runtimeApi.uploadArtifact({
|
||||
logger: stepCtx.logger,
|
||||
artifact: {
|
||||
name: `Maestro Test Device Logs`,
|
||||
paths: [deviceLogsDir],
|
||||
type: GenericArtifactType.OTHER,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
stepCtx.logger.error({ err }, 'Failed to upload device logs.');
|
||||
}
|
||||
}
|
||||
|
||||
stepCtx.logger.info('');
|
||||
|
||||
// If any tests failed, we throw an error to mark the step as failed.
|
||||
if (failedFlows.length > 0) {
|
||||
throw new Error(
|
||||
`Some Maestro tests failed:\n- ${failedFlows
|
||||
.map(flowPath => path.relative(stepCtx.workingDirectory, flowPath))
|
||||
.join('\n- ')}`
|
||||
);
|
||||
} else {
|
||||
stepCtx.logger.info('All Maestro tests passed.');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getMaestroTestCommand(params: {
|
||||
flow_path: string;
|
||||
output_format: string | undefined;
|
||||
/** Unused if `output_format` is undefined */
|
||||
output_path: string;
|
||||
}): [command: string, ...args: string[]] {
|
||||
let outputFormatFlags: string[] = [];
|
||||
if (params.output_format) {
|
||||
outputFormatFlags = [`--format`, params.output_format, `--output`, params.output_path];
|
||||
}
|
||||
|
||||
return ['maestro', 'test', ...outputFormatFlags, params.flow_path] as [
|
||||
command: string,
|
||||
...args: string[],
|
||||
];
|
||||
}
|
||||
|
||||
const MaestroOutputFormatToExtensionMap: Record<string, string | undefined> = {
|
||||
junit: 'xml',
|
||||
html: 'html',
|
||||
};
|
||||
|
||||
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
||||
const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
|
||||
|
||||
async function withCleanDeviceAsync<TResult>({
|
||||
platform,
|
||||
sourceDeviceIdentifier,
|
||||
localDeviceName,
|
||||
env,
|
||||
logger,
|
||||
fn,
|
||||
}: {
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
platform: 'ios' | 'android';
|
||||
sourceDeviceIdentifier: IosSimulatorName | AndroidVirtualDeviceName;
|
||||
localDeviceName: IosSimulatorName | AndroidVirtualDeviceName;
|
||||
fn: ({
|
||||
deviceIdentifier,
|
||||
}: {
|
||||
deviceIdentifier: IosSimulatorName | AndroidDeviceSerialId;
|
||||
}) => Promise<TResult>;
|
||||
}): Promise<{ fnResult: TResult; logsResult: Result<{ outputPath: string }> | null }> {
|
||||
// Clone and start the device
|
||||
|
||||
let localDeviceIdentifier: IosSimulatorName | AndroidDeviceSerialId | null = null;
|
||||
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
logger.info(`Cloning iOS Simulator ${sourceDeviceIdentifier} to ${localDeviceName}...`);
|
||||
await IosSimulatorUtils.cloneAsync({
|
||||
sourceDeviceIdentifier: sourceDeviceIdentifier as IosSimulatorName,
|
||||
destinationDeviceName: localDeviceName as IosSimulatorName,
|
||||
env,
|
||||
});
|
||||
logger.info(`Starting iOS Simulator ${localDeviceName}...`);
|
||||
const { udid } = await IosSimulatorUtils.startAsync({
|
||||
deviceIdentifier: localDeviceName as IosSimulatorName,
|
||||
env,
|
||||
});
|
||||
logger.info(`Waiting for iOS Simulator ${localDeviceName} to be ready...`);
|
||||
await IosSimulatorUtils.waitForReadyAsync({
|
||||
udid,
|
||||
env,
|
||||
});
|
||||
localDeviceIdentifier = localDeviceName as IosSimulatorName;
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
await retryAsync(
|
||||
async attemptCount => {
|
||||
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
||||
const attempt = attemptCount + 1;
|
||||
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
||||
let serialId: AndroidDeviceSerialId | null = null;
|
||||
try {
|
||||
logger.info(
|
||||
`Cloning Android Emulator ${sourceDeviceIdentifier} to ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`
|
||||
);
|
||||
await AndroidEmulatorUtils.cloneAsync({
|
||||
sourceDeviceName: sourceDeviceIdentifier as AndroidVirtualDeviceName,
|
||||
destinationDeviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
logger.info(
|
||||
`Starting Android Emulator ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`
|
||||
);
|
||||
const startResult = await AndroidEmulatorUtils.startAsync({
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
serialId = startResult.serialId;
|
||||
logger.info(`Waiting for Android Emulator ${localDeviceName} to be ready...`);
|
||||
await AndroidEmulatorUtils.waitForReadyAsync({
|
||||
serialId,
|
||||
env,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
localDeviceIdentifier = serialId;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
`Failed to start Android Emulator ${localDeviceName} on attempt ${attempt}/${maxAttempts}.`
|
||||
);
|
||||
try {
|
||||
if (serialId) {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId,
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
deviceName: localDeviceName as AndroidVirtualDeviceName,
|
||||
env,
|
||||
});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
logger.warn(
|
||||
{ err: cleanupErr },
|
||||
`Failed to clean up Android Emulator ${localDeviceName}.`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
{
|
||||
logger,
|
||||
retryOptions: {
|
||||
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!localDeviceIdentifier) {
|
||||
throw new Error('Device did not return an identifier after startup.');
|
||||
}
|
||||
|
||||
// Run the function
|
||||
|
||||
const fnResult = await asyncResult(fn({ deviceIdentifier: localDeviceIdentifier }));
|
||||
|
||||
// Stop the device
|
||||
|
||||
let logsResult: Result<{ outputPath: string }> | null = null;
|
||||
|
||||
try {
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
logger.info(`Collecting logs from ${localDeviceName}...`);
|
||||
logsResult = await asyncResult(
|
||||
IosSimulatorUtils.collectLogsAsync({
|
||||
deviceIdentifier: localDeviceIdentifier as IosSimulatorName,
|
||||
env,
|
||||
})
|
||||
);
|
||||
|
||||
logger.info(`Cleaning up ${localDeviceName}...`);
|
||||
await IosSimulatorUtils.deleteAsync({
|
||||
deviceIdentifier: localDeviceIdentifier as IosSimulatorName,
|
||||
env,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
logger.info(`Collecting logs from ${localDeviceName}...`);
|
||||
logsResult = await asyncResult(
|
||||
AndroidEmulatorUtils.collectLogsAsync({
|
||||
serialId: localDeviceIdentifier as unknown as AndroidDeviceSerialId,
|
||||
env,
|
||||
})
|
||||
);
|
||||
|
||||
logger.info(`Cleaning up ${localDeviceName}...`);
|
||||
await AndroidEmulatorUtils.deleteAsync({
|
||||
serialId: localDeviceIdentifier as unknown as AndroidDeviceSerialId,
|
||||
env,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error cleaning up device: ${err}`);
|
||||
}
|
||||
|
||||
return { fnResult: fnResult.enforceValue(), logsResult };
|
||||
}
|
||||
|
||||
/** Runs provided `fn` function, optionally wrapping it with starting and stopping screen recording. */
|
||||
async function maybeWithScreenRecordingAsync<TResult>({
|
||||
shouldRecord,
|
||||
platform,
|
||||
deviceIdentifier,
|
||||
env,
|
||||
logger,
|
||||
fn,
|
||||
}: {
|
||||
// As weird as it is, it's more convenient to have this function like `maybeWith...`
|
||||
// than "withScreenRecordingAsync" and `withScreenRecordingAsync(fn)` vs `fn` in the caller.
|
||||
shouldRecord: boolean;
|
||||
platform: 'ios' | 'android';
|
||||
deviceIdentifier: IosSimulatorName | AndroidDeviceSerialId;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
fn: () => Promise<TResult>;
|
||||
}): Promise<{ fnResult: Result<TResult>; recordingResult: Result<string | null> }> {
|
||||
if (!shouldRecord) {
|
||||
return { fnResult: await asyncResult(fn()), recordingResult: result(null) };
|
||||
}
|
||||
|
||||
let recordingResult: Result<{
|
||||
recordingSpawn: SpawnPromise<SpawnResult>;
|
||||
outputPath?: string;
|
||||
}>;
|
||||
|
||||
// Start screen recording
|
||||
|
||||
logger.info(`Starting screen recording on ${deviceIdentifier}...`);
|
||||
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
recordingResult = await asyncResult(
|
||||
IosSimulatorUtils.startScreenRecordingAsync({
|
||||
deviceIdentifier: deviceIdentifier as IosSimulatorName,
|
||||
env,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
recordingResult = await asyncResult(
|
||||
AndroidEmulatorUtils.startScreenRecordingAsync({
|
||||
serialId: deviceIdentifier as AndroidDeviceSerialId,
|
||||
env,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recordingResult.ok) {
|
||||
logger.warn('Failed to start screen recording.', recordingResult.reason);
|
||||
}
|
||||
|
||||
// Run the function
|
||||
|
||||
const fnResult = await asyncResult(fn());
|
||||
|
||||
// If recording failed there's nothing to stop, so we return the results
|
||||
|
||||
if (!recordingResult.ok) {
|
||||
return { fnResult, recordingResult: result(recordingResult.reason) };
|
||||
}
|
||||
|
||||
// If recording started, finish it
|
||||
|
||||
try {
|
||||
logger.info(`Stopping screen recording on ${deviceIdentifier}...`);
|
||||
|
||||
switch (platform) {
|
||||
case 'ios': {
|
||||
await IosSimulatorUtils.stopScreenRecordingAsync({
|
||||
recordingSpawn: recordingResult.value.recordingSpawn,
|
||||
});
|
||||
return {
|
||||
fnResult,
|
||||
// We know outputPath is defined, because startIosScreenRecording() should have filled it.
|
||||
recordingResult: result(recordingResult.value.outputPath!),
|
||||
};
|
||||
}
|
||||
case 'android': {
|
||||
const { outputPath } = await AndroidEmulatorUtils.stopScreenRecordingAsync({
|
||||
serialId: deviceIdentifier as AndroidDeviceSerialId,
|
||||
recordingSpawn: recordingResult.value.recordingSpawn,
|
||||
env,
|
||||
});
|
||||
return { fnResult, recordingResult: result(outputPath) };
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to stop screen recording.', err);
|
||||
|
||||
return { fnResult, recordingResult: result(err as Error) };
|
||||
}
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import bunyan from 'bunyan';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../findMaestroPathsFlowsToExecuteAsync';
|
||||
|
||||
// We use node:fs
|
||||
jest.unmock('node:fs');
|
||||
// and fg uses fs.
|
||||
jest.unmock('fs');
|
||||
jest.setTimeout(15000);
|
||||
|
||||
/**
|
||||
*
|
||||
* You need to have a Simulator running to run these tests.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('findMaestroPathsFlowsToExecuteAsync', () => {
|
||||
const fixturesDir = path.join(__dirname, 'fixtures', 'maestro-flows');
|
||||
const singleFileDir = path.join(__dirname, 'fixtures', 'single-file');
|
||||
const configFlowsDir = path.join(__dirname, 'fixtures', 'config-flows');
|
||||
const noConfigFlowsDir = path.join(__dirname, 'fixtures', 'no-config-flows');
|
||||
|
||||
let logger: bunyan;
|
||||
|
||||
beforeAll(() => {
|
||||
logger = createMockLogger();
|
||||
});
|
||||
|
||||
// Helper function to run maestro test and determine which flows it executed
|
||||
// by parsing the output
|
||||
async function getMaestroFlowList(
|
||||
flowPath: string,
|
||||
includeTags: string[] = [],
|
||||
excludeTags: string[] = []
|
||||
): Promise<string[]> {
|
||||
const args = ['test', flowPath, '--no-ansi'];
|
||||
|
||||
if (includeTags.length > 0) {
|
||||
args.push('--include-tags', includeTags.join(','));
|
||||
}
|
||||
|
||||
if (excludeTags.length > 0) {
|
||||
args.push('--exclude-tags', excludeTags.join(','));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await spawnAsync('maestro', args, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
return parseMaestroExecutedFlows(result.stdout + result.stderr);
|
||||
} catch (error: any) {
|
||||
const output = (error.stdout || '') + (error.stderr || '') + (error.message || '');
|
||||
return parseMaestroExecutedFlows(output);
|
||||
}
|
||||
}
|
||||
|
||||
function parseMaestroExecutedFlows(output: string): string[] {
|
||||
return output
|
||||
.split('\n')
|
||||
.filter(line => line.includes('[Passed]') || line.includes('> Flow'))
|
||||
.map(line => {
|
||||
if (line.includes('[Passed]')) {
|
||||
return line.split('[Passed]')[1].trim().split(' ')[0];
|
||||
}
|
||||
return line.split('> Flow')[1].trim();
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
// Helper function to run our implementation
|
||||
async function getOurFlowList(
|
||||
flowPath: string,
|
||||
includeTags: string[] = [],
|
||||
excludeTags: string[] = []
|
||||
): Promise<string[]> {
|
||||
const result = await findMaestroPathsFlowsToExecuteAsync({
|
||||
workingDirectory: process.cwd(),
|
||||
flowPath: path.relative(process.cwd(), flowPath),
|
||||
includeTags,
|
||||
excludeTags,
|
||||
logger,
|
||||
});
|
||||
|
||||
return result.map(filePath => path.basename(filePath, path.extname(filePath))).sort();
|
||||
}
|
||||
|
||||
describe('Directory-based flow discovery', () => {
|
||||
it('should discover all flows when no tags are specified', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir);
|
||||
const ourFlows = await getOurFlowList(fixturesDir);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should filter flows by include tags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, ['auth']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, ['auth']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should filter flows by exclude tags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, [], ['slow']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, [], ['slow']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should filter flows by both include and exclude tags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, ['smoke'], ['regression']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, ['smoke'], ['regression']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should handle multiple include tags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, ['auth', 'smoke']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, ['auth', 'smoke']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should handle multiple exclude tags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, [], ['slow', 'regression']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, [], ['slow', 'regression']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should not include nested flows in subdirectories', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(fixturesDir, ['nested']);
|
||||
const ourFlows = await getOurFlowList(fixturesDir, ['nested']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
});
|
||||
|
||||
it('should exclude config files from flow discovery', async () => {
|
||||
const ourFlows = await getOurFlowList(fixturesDir);
|
||||
|
||||
expect(ourFlows.every(flow => !flow.endsWith('config.yaml'))).toBe(true);
|
||||
expect(ourFlows.every(flow => !flow.endsWith('config.yml'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude non-YAML files from flow discovery', async () => {
|
||||
const ourFlows = await getOurFlowList(fixturesDir);
|
||||
|
||||
expect(ourFlows).not.toContain('non-flow-file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single file scenarios', () => {
|
||||
it('should handle single file input', async () => {
|
||||
const singleFile = path.join(singleFileDir, 'standalone-flow.yaml');
|
||||
|
||||
const maestroFlows = await getMaestroFlowList(singleFile);
|
||||
const ourFlows = await getOurFlowList(singleFile);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
expect(ourFlows).toHaveLength(1);
|
||||
expect(ourFlows[0]).toBe(path.basename(singleFile, path.extname(singleFile)));
|
||||
});
|
||||
|
||||
it('should handle single file with tag filtering (include)', async () => {
|
||||
const singleFile = path.join(singleFileDir, 'standalone-flow.yaml');
|
||||
|
||||
// This file has 'smoke' tag, so should be included
|
||||
const maestroFlows = await getMaestroFlowList(singleFile, ['smoke']);
|
||||
const ourFlows = await getOurFlowList(singleFile, ['smoke']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
expect(ourFlows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle single file with tag filtering (exclude)', async () => {
|
||||
const singleFile = path.join(singleFileDir, 'standalone-flow.yaml');
|
||||
|
||||
// This file has 'smoke' tag, so should be excluded
|
||||
const maestroFlows = await getMaestroFlowList(singleFile, [], ['smoke']);
|
||||
const ourFlows = await getOurFlowList(singleFile, [], ['smoke']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
expect(ourFlows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle non-existent paths gracefully', async () => {
|
||||
const nonExistentPath = '/path/that/does/not/exist';
|
||||
|
||||
// Our implementation should return empty array for non-existent paths
|
||||
await expect(getOurFlowList(nonExistentPath)).rejects.toThrow('ENOENT');
|
||||
});
|
||||
|
||||
it('should handle empty directories', async () => {
|
||||
const emptyDir = path.join(__dirname, 'fixtures', 'empty-dir');
|
||||
await fs.mkdir(emptyDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const ourFlows = await getOurFlowList(emptyDir);
|
||||
expect(ourFlows).toEqual([]);
|
||||
} finally {
|
||||
await fs.rmdir(emptyDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle flows that cannot be parsed', async () => {
|
||||
// The invalid-flow.yaml has extra fields that might cause parsing issues
|
||||
// but should still be discovered if tag filtering passes
|
||||
const ourFlows = await getOurFlowList(fixturesDir, ['invalid']);
|
||||
|
||||
// Should either include the file (if parsing is lenient) or exclude it (if parsing fails)
|
||||
// The important thing is that it doesn't crash
|
||||
expect(Array.isArray(ourFlows)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance comparison', () => {
|
||||
it('should complete discovery in reasonable time', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await getOurFlowList(fixturesDir);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
|
||||
});
|
||||
});
|
||||
|
||||
describe('Detailed flow content verification', () => {
|
||||
it('should correctly identify flows with specific tag combinations', async () => {
|
||||
// Test complex tag filtering scenarios
|
||||
const authFlows = await getOurFlowList(fixturesDir, ['auth']);
|
||||
const smokeFlows = await getOurFlowList(fixturesDir, ['smoke']);
|
||||
const criticalFlows = await getOurFlowList(fixturesDir, ['critical']);
|
||||
|
||||
// Verify specific files are included/excluded correctly
|
||||
expect(authFlows).toContain('tagged-flow-auth');
|
||||
expect(authFlows).not.toContain('nested-flow'); // Subdirectory flows excluded by default
|
||||
expect(smokeFlows).toContain('tagged-flow-smoke');
|
||||
expect(criticalFlows).toContain('tagged-flow-smoke');
|
||||
});
|
||||
|
||||
it('should handle flows without tags correctly', async () => {
|
||||
// When no filters are applied, flows without tags should be included
|
||||
const allFlows = await getOurFlowList(fixturesDir);
|
||||
expect(allFlows).toContain('no-tags-flow');
|
||||
expect(allFlows).toContain('basic-flow');
|
||||
|
||||
// When include tags are specified, flows without tags should be excluded
|
||||
const taggedFlows = await getOurFlowList(fixturesDir, ['auth']);
|
||||
expect(taggedFlows).not.toContain('no-tags-flow');
|
||||
expect(taggedFlows).not.toContain('basic-flow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('config.yaml flow patterns', () => {
|
||||
it('should discover flows using config.yaml patterns', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(configFlowsDir);
|
||||
const ourFlows = await getOurFlowList(configFlowsDir);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
|
||||
// Verify specific files are included based on config patterns
|
||||
expect(ourFlows).toContain('login'); // features/auth/login.yaml
|
||||
expect(ourFlows).toContain('logout'); // features/auth/logout.yaml
|
||||
expect(ourFlows).toContain('navigation'); // features/core/navigation.yaml
|
||||
expect(ourFlows).toContain('api.spec'); // specs/api.spec.yaml
|
||||
expect(ourFlows).toContain('ui.spec'); // specs/ui.spec.yaml
|
||||
|
||||
// Verify top-level file is excluded (not matching patterns)
|
||||
expect(ourFlows).not.toContain('ignored-top-level');
|
||||
});
|
||||
|
||||
it('should handle tag filtering with config.yaml patterns', async () => {
|
||||
const authFlows = await getOurFlowList(configFlowsDir, ['auth']);
|
||||
const maestroAuthFlows = await getMaestroFlowList(configFlowsDir, ['auth']);
|
||||
|
||||
expect(authFlows).toEqual(maestroAuthFlows);
|
||||
expect(authFlows).toContain('login');
|
||||
expect(authFlows).toContain('logout');
|
||||
expect(authFlows).not.toContain('navigation');
|
||||
expect(authFlows).not.toContain('api.spec');
|
||||
});
|
||||
|
||||
it('should handle exclude tags with config.yaml patterns', async () => {
|
||||
const nonSmokeFlows = await getOurFlowList(configFlowsDir, [], ['smoke']);
|
||||
const maestroNonSmokeFlows = await getMaestroFlowList(configFlowsDir, [], ['smoke']);
|
||||
|
||||
expect(nonSmokeFlows).toEqual(maestroNonSmokeFlows);
|
||||
expect(nonSmokeFlows).not.toContain('login'); // has smoke tag
|
||||
expect(nonSmokeFlows).not.toContain('navigation'); // has smoke tag
|
||||
expect(nonSmokeFlows).toContain('logout'); // no smoke tag
|
||||
expect(nonSmokeFlows).toContain('api.spec'); // no smoke tag
|
||||
});
|
||||
|
||||
it('should handle multiple include tags with config.yaml patterns', async () => {
|
||||
const multiTagFlows = await getOurFlowList(configFlowsDir, ['auth', 'integration']);
|
||||
const maestroMultiTagFlows = await getMaestroFlowList(configFlowsDir, [
|
||||
'auth',
|
||||
'integration',
|
||||
]);
|
||||
|
||||
expect(multiTagFlows).toEqual(maestroMultiTagFlows);
|
||||
expect(multiTagFlows).toContain('login'); // auth tag
|
||||
expect(multiTagFlows).toContain('logout'); // auth tag
|
||||
expect(multiTagFlows).toContain('api.spec'); // integration tag
|
||||
expect(multiTagFlows).toContain('ui.spec'); // integration tag
|
||||
expect(multiTagFlows).not.toContain('navigation'); // only core/smoke tags
|
||||
});
|
||||
});
|
||||
|
||||
describe('config.yaml includeTags/excludeTags merging', () => {
|
||||
const configTagsDir = path.join(__dirname, 'fixtures', 'config-tags');
|
||||
|
||||
it('should respect includeTags/excludeTags from config.yaml when no CLI tags provided', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(configTagsDir);
|
||||
const ourFlows = await getOurFlowList(configTagsDir);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
|
||||
// Based on fixtures, only flows matching config includeTags and not matching excludeTags should be included
|
||||
expect(ourFlows).toContain('flow-inc-only');
|
||||
expect(ourFlows).not.toContain('flow-exc-only');
|
||||
expect(ourFlows).not.toContain('flow-inc-and-exc');
|
||||
expect(ourFlows).not.toContain('flow-neutral');
|
||||
expect(ourFlows).not.toContain('flow-cli-exclude');
|
||||
expect(ourFlows).not.toContain('flow-cli-include');
|
||||
});
|
||||
|
||||
it('should merge CLI includeTags with config includeTags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(configTagsDir, ['cliInclude']);
|
||||
const ourFlows = await getOurFlowList(configTagsDir, ['cliInclude']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
// Now flows with either cfgInclude or cliInclude should be included, minus excluded ones
|
||||
expect(ourFlows).toContain('flow-inc-only');
|
||||
expect(ourFlows).toContain('flow-cli-include');
|
||||
expect(ourFlows).not.toContain('flow-exc-only');
|
||||
expect(ourFlows).not.toContain('flow-inc-and-exc');
|
||||
expect(ourFlows).not.toContain('flow-cli-exclude');
|
||||
});
|
||||
|
||||
it('should merge CLI excludeTags with config excludeTags', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(configTagsDir, [], ['cliExclude']);
|
||||
const ourFlows = await getOurFlowList(configTagsDir, [], ['cliExclude']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
// Exclude both cfgExclude and cliExclude
|
||||
expect(ourFlows).toContain('flow-inc-only');
|
||||
expect(ourFlows).not.toContain('flow-exc-only');
|
||||
expect(ourFlows).not.toContain('flow-cli-exclude');
|
||||
expect(ourFlows).not.toContain('flow-inc-and-exc');
|
||||
});
|
||||
|
||||
it('should apply both merged include and exclude tags correctly', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(configTagsDir, ['cfgInclude'], ['cfgExclude']);
|
||||
const ourFlows = await getOurFlowList(configTagsDir, ['cfgInclude'], ['cfgExclude']);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
expect(ourFlows).toContain('flow-inc-only');
|
||||
expect(ourFlows).not.toContain('flow-exc-only');
|
||||
expect(ourFlows).not.toContain('flow-inc-and-exc');
|
||||
expect(ourFlows).not.toContain('flow-neutral');
|
||||
});
|
||||
});
|
||||
|
||||
describe('backward compatibility (no config.yaml)', () => {
|
||||
it('should use default "*" pattern when no config.yaml exists', async () => {
|
||||
const maestroFlows = await getMaestroFlowList(noConfigFlowsDir);
|
||||
const ourFlows = await getOurFlowList(noConfigFlowsDir);
|
||||
|
||||
expect(ourFlows).toEqual(maestroFlows);
|
||||
|
||||
// Should include top-level files
|
||||
expect(ourFlows).toContain('top-level-flow');
|
||||
|
||||
// Should exclude subdirectory files (default "*" behavior)
|
||||
expect(ourFlows).not.toContain('nested-flow');
|
||||
});
|
||||
|
||||
it('should handle tag filtering without config.yaml', async () => {
|
||||
const basicFlows = await getOurFlowList(noConfigFlowsDir, ['basic']);
|
||||
const maestroBasicFlows = await getMaestroFlowList(noConfigFlowsDir, ['basic']);
|
||||
|
||||
expect(basicFlows).toEqual(maestroBasicFlows);
|
||||
expect(basicFlows).toContain('top-level-flow');
|
||||
|
||||
const noIncludeTagsFlows = await getOurFlowList(noConfigFlowsDir, []);
|
||||
const maestroNoIncludeTagsFlows = await getMaestroFlowList(noConfigFlowsDir, []);
|
||||
|
||||
expect(noIncludeTagsFlows).toEqual(maestroNoIncludeTagsFlows);
|
||||
expect(noIncludeTagsFlows).toContain('top-level-flow');
|
||||
expect(noIncludeTagsFlows).not.toContain('nested-flow');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
flows:
|
||||
- 'features/**/*.yaml'
|
||||
- 'specs/*.spec.yaml'
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "login"
|
||||
tags: ["auth", "smoke"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "logout"
|
||||
tags: ["auth"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "navigation"
|
||||
tags: ["core", "smoke"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "ignored-top-level"
|
||||
tags: ["ignored"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "api.spec"
|
||||
tags: ["api", "integration"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "ui.spec"
|
||||
tags: ["ui", "integration"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
appId: com.example.app
|
||||
name: "basic-flow"
|
||||
---
|
||||
- assertTrue: true
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
appId: com.example.app
|
||||
name: "invalid-flow"
|
||||
tags:
|
||||
- invalid
|
||||
---
|
||||
- assertTrue: true
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
appId: com.example.app
|
||||
name: "no-tags-flow"
|
||||
---
|
||||
- assertTrue: true
|
||||
+1
@@ -0,0 +1 @@
|
||||
This is not a YAML file and should be ignored by flow discovery.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
appId: com.example.app
|
||||
name: "nested-flow"
|
||||
tags:
|
||||
- nested
|
||||
- auth
|
||||
---
|
||||
- assertTrue: true
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
appId: com.example.app
|
||||
name: "tagged-flow-auth"
|
||||
tags:
|
||||
- auth
|
||||
- login
|
||||
---
|
||||
- assertTrue: true
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
appId: com.example.app
|
||||
name: "tagged-flow-regression"
|
||||
tags:
|
||||
- regression
|
||||
- slow
|
||||
---
|
||||
- assertTrue: true
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
appId: com.example.app
|
||||
name: "tagged-flow-smoke"
|
||||
tags:
|
||||
- smoke
|
||||
- critical
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "nested-flow"
|
||||
tags: ["nested"]
|
||||
---
|
||||
- assertTrue: true
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
appId: com.example.app
|
||||
name: "top-level-flow"
|
||||
tags: ["basic"]
|
||||
---
|
||||
- assertTrue: true
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
appId: com.example.app
|
||||
name: "standalone-flow"
|
||||
tags:
|
||||
- standalone
|
||||
- smoke
|
||||
---
|
||||
- assertTrue: true
|
||||
@@ -0,0 +1,120 @@
|
||||
import fs from 'fs-extra';
|
||||
import nock from 'nock';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
|
||||
jest.unmock('fs');
|
||||
jest.unmock('node:fs');
|
||||
jest.unmock('fs/promises');
|
||||
jest.unmock('node:fs/promises');
|
||||
|
||||
describe('precompiledModules integration', () => {
|
||||
beforeAll(() => {
|
||||
nock.disableNetConnect();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('merges the extracted debug and release xcframework trees', async () => {
|
||||
const homeDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'precompiled-modules-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = homeDirectory;
|
||||
|
||||
try {
|
||||
const fixturesDirectory = path.join(__dirname, 'fixtures', 'precompiledModules');
|
||||
const debugArchive = await fs.readFile(
|
||||
path.join(fixturesDirectory, 'xcframeworks-Debug.zip')
|
||||
);
|
||||
const releaseArchive = await fs.readFile(
|
||||
path.join(fixturesDirectory, 'xcframeworks-Release.zip')
|
||||
);
|
||||
|
||||
nock('https://fixtures.expo.test')
|
||||
.get('/xcframeworks-Debug.zip')
|
||||
.reply(200, debugArchive, { 'Content-Type': 'application/zip' })
|
||||
.get('/xcframeworks-Release.zip')
|
||||
.reply(200, releaseArchive, { 'Content-Type': 'application/zip' });
|
||||
|
||||
jest.resetModules();
|
||||
jest.doMock('os', () => ({
|
||||
...jest.requireActual('os'),
|
||||
homedir: () => homeDirectory,
|
||||
}));
|
||||
const {
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
startPreparingPrecompiledDependencies,
|
||||
waitForPrecompiledModulesPreparationAsync,
|
||||
} = await import('../precompiledModules');
|
||||
|
||||
startPreparingPrecompiledDependencies(
|
||||
{
|
||||
logger: createMockLogger(),
|
||||
env: {},
|
||||
} as any,
|
||||
[
|
||||
'https://fixtures.expo.test/xcframeworks-Debug.zip',
|
||||
'https://fixtures.expo.test/xcframeworks-Release.zip',
|
||||
]
|
||||
);
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
|
||||
expect(PRECOMPILED_MODULES_PATH).toBe(
|
||||
path.join(homeDirectory, '.expo', 'precompiled-modules')
|
||||
);
|
||||
|
||||
await expect(
|
||||
fs.readFile(
|
||||
path.join(
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
'expo-modules-core/output/debug/xcframeworks/ExpoModulesCore.tar.gz'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
).resolves.toBe('debug expo-modules-core fixture\n');
|
||||
await expect(
|
||||
fs.readFile(
|
||||
path.join(
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
'expo-modules-core/output/release/xcframeworks/ExpoModulesCore.tar.gz'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
).resolves.toBe('release expo-modules-core fixture\n');
|
||||
await expect(
|
||||
fs.readFile(
|
||||
path.join(
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
'expo-camera/output/debug/xcframeworks/ExpoCamera.tar.gz'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
).resolves.toBe('debug expo-camera fixture\n');
|
||||
await expect(
|
||||
fs.readFile(
|
||||
path.join(
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
'expo-application/output/release/xcframeworks/EXApplication.tar.gz'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
).resolves.toBe('release expo-application fixture\n');
|
||||
} finally {
|
||||
jest.dontMock('os');
|
||||
jest.resetModules();
|
||||
if (previousHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = previousHome;
|
||||
}
|
||||
await fs.remove(homeDirectory);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import downloadFile from '@expo/downloader';
|
||||
import { mkdirp, mkdtemp, move, remove } from 'fs-extra';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
|
||||
import {
|
||||
PRECOMPILED_MODULES_PATH,
|
||||
startPreparingPrecompiledDependencies,
|
||||
waitForPrecompiledModulesPreparationAsync,
|
||||
} from '../precompiledModules';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { BuildContext } from '../../context';
|
||||
|
||||
jest.mock('@expo/downloader');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('node-stream-zip', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
async: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('precompiledModules', () => {
|
||||
const extract = jest.fn();
|
||||
const close = jest.fn();
|
||||
const getMkdtempPath = jest.fn();
|
||||
|
||||
const createCtx = (env: Record<string, string | undefined> = {}): BuildContext =>
|
||||
({
|
||||
logger: createMockLogger(),
|
||||
env,
|
||||
}) as unknown as BuildContext;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(remove).mockImplementation(async () => undefined as any);
|
||||
jest.mocked(mkdirp).mockImplementation(async () => undefined as any);
|
||||
jest.mocked(move).mockImplementation(async () => undefined as any);
|
||||
getMkdtempPath.mockReset();
|
||||
getMkdtempPath
|
||||
.mockReturnValueOnce('/tmp/precompiled-modules-archive')
|
||||
.mockReturnValueOnce('/tmp/precompiled-modules-staging');
|
||||
jest.mocked(mkdtemp).mockImplementation(async () => getMkdtempPath() as any);
|
||||
jest.mocked(downloadFile).mockReset();
|
||||
extract.mockReset().mockResolvedValue(undefined);
|
||||
close.mockReset().mockResolvedValue(undefined);
|
||||
jest.mocked(StreamZip.async).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
extract,
|
||||
close,
|
||||
}) as any
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('downloads through CocoaPods proxy when enabled', async () => {
|
||||
jest.mocked(downloadFile).mockResolvedValue(undefined);
|
||||
|
||||
startPreparingPrecompiledDependencies(
|
||||
createCtx({ EAS_BUILD_COCOAPODS_CACHE_URL: 'http://localhost:9001' }),
|
||||
[
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
]
|
||||
);
|
||||
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
|
||||
expect(downloadFile).toHaveBeenCalledWith(
|
||||
'http://localhost:9001/storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
'/tmp/precompiled-modules-archive/precompiled-modules-0.zip',
|
||||
{ retry: 3 }
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the direct url when CocoaPods proxy download fails', async () => {
|
||||
jest
|
||||
.mocked(downloadFile)
|
||||
.mockRejectedValueOnce(new Error('proxy failed'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
startPreparingPrecompiledDependencies(
|
||||
createCtx({ EAS_BUILD_COCOAPODS_CACHE_URL: 'http://localhost:9001' }),
|
||||
[
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
]
|
||||
);
|
||||
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
|
||||
expect(downloadFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'http://localhost:9001/storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
'/tmp/precompiled-modules-archive/precompiled-modules-0.zip',
|
||||
{ retry: 3 }
|
||||
);
|
||||
expect(downloadFile).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
'/tmp/precompiled-modules-archive/precompiled-modules-0.zip',
|
||||
{ retry: 3 }
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts all configured archives into the well-known destination path', async () => {
|
||||
jest.mocked(downloadFile).mockResolvedValue(undefined);
|
||||
|
||||
startPreparingPrecompiledDependencies(createCtx(), [
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-1.zip',
|
||||
]);
|
||||
|
||||
await waitForPrecompiledModulesPreparationAsync();
|
||||
|
||||
expect(mkdirp).toHaveBeenCalledWith(PRECOMPILED_MODULES_PATH);
|
||||
expect(downloadFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
'/tmp/precompiled-modules-archive/precompiled-modules-0.zip',
|
||||
{ retry: 3 }
|
||||
);
|
||||
expect(downloadFile).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-1.zip',
|
||||
'/tmp/precompiled-modules-archive/precompiled-modules-1.zip',
|
||||
{ retry: 3 }
|
||||
);
|
||||
expect(extract).toHaveBeenNthCalledWith(1, null, '/tmp/precompiled-modules-staging');
|
||||
expect(extract).toHaveBeenNthCalledWith(2, null, '/tmp/precompiled-modules-staging');
|
||||
expect(move).toHaveBeenCalledWith('/tmp/precompiled-modules-staging', PRECOMPILED_MODULES_PATH);
|
||||
});
|
||||
|
||||
it('leaves the destination empty when preparation fails', async () => {
|
||||
jest.mocked(downloadFile).mockResolvedValue(undefined);
|
||||
extract.mockRejectedValueOnce(new Error('extract failed'));
|
||||
|
||||
startPreparingPrecompiledDependencies(createCtx(), [
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules-production/ios/precompiled-modules-0.zip',
|
||||
]);
|
||||
|
||||
await expect(waitForPrecompiledModulesPreparationAsync()).rejects.toThrow('extract failed');
|
||||
|
||||
expect(move).not.toHaveBeenCalled();
|
||||
expect(remove).toHaveBeenCalledWith('/tmp/precompiled-modules-staging');
|
||||
expect(remove).toHaveBeenCalledWith(PRECOMPILED_MODULES_PATH);
|
||||
expect(mkdirp).toHaveBeenCalledWith(PRECOMPILED_MODULES_PATH);
|
||||
});
|
||||
|
||||
it('throws when precompiled dependencies are still not ready after 30 seconds', async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.mocked(downloadFile).mockImplementation(() => new Promise<void>(() => {}) as any);
|
||||
|
||||
startPreparingPrecompiledDependencies(createCtx(), [
|
||||
'https://example.com/xcframeworks-Debug.zip',
|
||||
]);
|
||||
|
||||
const waitPromise = waitForPrecompiledModulesPreparationAsync();
|
||||
const waitExpectation = expect(waitPromise).rejects.toThrow(
|
||||
'Timed out waiting for precompiled dependencies after 30 seconds'
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
await jest.advanceTimersByTimeAsync(30_000);
|
||||
await waitExpectation;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import fg from 'fast-glob';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as yaml from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
const FlowConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const WorkspaceConfigSchema = z.object({
|
||||
flows: z.array(z.string()).optional(),
|
||||
executionOrder: z.record(z.string(), z.unknown()).optional(),
|
||||
includeTags: z.array(z.string()).optional(),
|
||||
excludeTags: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
type FlowConfig = z.infer<typeof FlowConfigSchema>;
|
||||
type WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>;
|
||||
|
||||
export async function findMaestroPathsFlowsToExecuteAsync({
|
||||
workingDirectory,
|
||||
flowPath,
|
||||
includeTags: _includeTags,
|
||||
excludeTags: _excludeTags,
|
||||
logger,
|
||||
}: {
|
||||
workingDirectory: string;
|
||||
flowPath: string;
|
||||
includeTags: string[] | undefined;
|
||||
excludeTags: string[] | undefined;
|
||||
logger: bunyan;
|
||||
}): Promise<string[]> {
|
||||
const absoluteFlowPath = path.resolve(workingDirectory, flowPath);
|
||||
// If it's a file, just return it (no validation needed)
|
||||
const stat = await fs.stat(absoluteFlowPath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
logger.info(`Found a file: ${path.relative(workingDirectory, absoluteFlowPath)}`);
|
||||
return [absoluteFlowPath];
|
||||
}
|
||||
|
||||
// It's a directory - discover flow files
|
||||
logger.info(`Found a directory: ${path.relative(workingDirectory, absoluteFlowPath)}`);
|
||||
|
||||
// Check for workspace config
|
||||
logger.info(`Searching for workspace config...`);
|
||||
const workspaceConfig = await findAndParseWorkspaceConfigAsync({
|
||||
dirPath: absoluteFlowPath,
|
||||
workingDirectory,
|
||||
logger,
|
||||
});
|
||||
logger.info(`Using workspace config: ${JSON.stringify(workspaceConfig)}`);
|
||||
|
||||
if (workspaceConfig?.executionOrder) {
|
||||
logger.warn(`Execution order is not supported yet. Ignoring.`);
|
||||
}
|
||||
|
||||
logger.info(`Searching for flow files...`);
|
||||
const { flows } = await findAndParseFlowFilesAsync({
|
||||
dirPath: absoluteFlowPath,
|
||||
workingDirectory,
|
||||
workspaceConfig,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (flows.length === 0) {
|
||||
logger.info(
|
||||
`No valid flow files found in: ${path.relative(workingDirectory, absoluteFlowPath)}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const includeTags = [...(_includeTags ?? []), ...(workspaceConfig?.includeTags ?? [])];
|
||||
const excludeTags = [...(_excludeTags ?? []), ...(workspaceConfig?.excludeTags ?? [])];
|
||||
|
||||
if (includeTags.length === 0 && excludeTags.length === 0) {
|
||||
logger.info(`No tags provided, returning all flows.`);
|
||||
return flows.map(({ path }) => path);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Filtering flows by tags. Tags to include: ${JSON.stringify(includeTags)}. Tags to exclude: ${
|
||||
JSON.stringify(excludeTags) ?? 'none'
|
||||
}.`
|
||||
);
|
||||
return flows
|
||||
.filter(({ config, path: flowPath }) => {
|
||||
const shouldInclude = matchesTags({
|
||||
flowTags: config?.tags ?? [],
|
||||
includeTags,
|
||||
excludeTags,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
shouldInclude
|
||||
? `- ${path.relative(workingDirectory, flowPath)} matches tags, including.`
|
||||
: `- ${path.relative(workingDirectory, flowPath)} does not match tags, excluding.`
|
||||
);
|
||||
|
||||
return shouldInclude;
|
||||
})
|
||||
.map(({ path }) => path);
|
||||
}
|
||||
|
||||
async function findAndParseWorkspaceConfigAsync({
|
||||
dirPath,
|
||||
workingDirectory,
|
||||
logger,
|
||||
}: {
|
||||
dirPath: string;
|
||||
workingDirectory: string;
|
||||
logger: bunyan;
|
||||
}): Promise<WorkspaceConfig | null> {
|
||||
const configPaths = await fg(['config.yaml', 'config.yml'], {
|
||||
cwd: dirPath,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (configPaths.length === 0) {
|
||||
logger.info(`No workspace config found in: ${path.relative(workingDirectory, dirPath)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
try {
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
const configDoc = yaml.parse(content);
|
||||
if (!configDoc) {
|
||||
logger.warn(
|
||||
`No content found in workspace config: ${path.relative(workingDirectory, configPath)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
logger.info(`Using workspace config from: ${path.relative(workingDirectory, configPath)}`);
|
||||
return WorkspaceConfigSchema.parse(configDoc);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
`Failed to parse workspace config: ${path.relative(workingDirectory, configPath)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`No valid workspace config found in: ${path.relative(workingDirectory, dirPath)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findAndParseFlowFilesAsync({
|
||||
workingDirectory,
|
||||
dirPath,
|
||||
workspaceConfig,
|
||||
logger,
|
||||
}: {
|
||||
workingDirectory: string;
|
||||
dirPath: string;
|
||||
workspaceConfig: WorkspaceConfig | null;
|
||||
logger: bunyan;
|
||||
}): Promise<{ flows: { config: FlowConfig; path: string }[] }> {
|
||||
const flows: { config: FlowConfig; path: string }[] = [];
|
||||
|
||||
// Determine flow patterns from config or use default
|
||||
const flowPatterns = workspaceConfig?.flows ?? ['*'];
|
||||
logger.info(`Using flow patterns: ${JSON.stringify(flowPatterns)}`);
|
||||
|
||||
// Use fast-glob to find matching files
|
||||
const matchedFiles = await fg(flowPatterns, {
|
||||
cwd: dirPath,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
ignore: ['*/config.yaml', '*/config.yml'], // Skip workspace config files
|
||||
});
|
||||
|
||||
logger.info(`Found ${matchedFiles.length} potential flow files`);
|
||||
|
||||
// Parse each matched file
|
||||
for (const filePath of matchedFiles) {
|
||||
// Skip non-YAML files
|
||||
const ext = path.extname(filePath);
|
||||
if (ext !== '.yaml' && ext !== '.yml') {
|
||||
logger.info(`Skipping non-YAML file: ${path.relative(workingDirectory, filePath)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await asyncResult(parseFlowFile(filePath));
|
||||
if (result.ok) {
|
||||
logger.info(`Found flow file: ${path.relative(workingDirectory, filePath)}`);
|
||||
flows.push({ config: result.value, path: filePath });
|
||||
} else {
|
||||
logger.info(
|
||||
{ err: result.reason },
|
||||
`Skipping flow file: ${path.relative(workingDirectory, filePath)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { flows };
|
||||
}
|
||||
|
||||
async function parseFlowFile(filePath: string): Promise<FlowConfig> {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const documents = yaml.parseAllDocuments(content);
|
||||
const configDoc = documents[0];
|
||||
if (!configDoc) {
|
||||
throw new Error(`No config section found in ${filePath}`);
|
||||
}
|
||||
return FlowConfigSchema.parse(configDoc.toJS());
|
||||
}
|
||||
|
||||
function matchesTags({
|
||||
flowTags,
|
||||
includeTags,
|
||||
excludeTags,
|
||||
}: {
|
||||
flowTags: string[];
|
||||
includeTags: string[];
|
||||
excludeTags: string[];
|
||||
}): boolean {
|
||||
// Include logic: if includeTags is empty OR flow has any of the include tags
|
||||
const includeMatch = includeTags.length === 0 || includeTags.some(tag => flowTags.includes(tag));
|
||||
|
||||
// Exclude logic: if excludeTags is empty OR flow has none of the exclude tags
|
||||
const excludeMatch = excludeTags.length === 0 || !excludeTags.some(tag => flowTags.includes(tag));
|
||||
|
||||
return includeMatch && excludeMatch;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import downloadFile from '@expo/downloader';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
let precompiledModulesPreparationPromise: Promise<void> | null = null;
|
||||
const PRECOMPILED_MODULES_WAIT_TIMEOUT_MS = 30_000;
|
||||
export const PRECOMPILED_MODULES_PATH = path.join(os.homedir(), '.expo', 'precompiled-modules');
|
||||
|
||||
export function shouldUsePrecompiledDependencies(env: Record<string, string | undefined>): boolean {
|
||||
return env.EAS_USE_PRECOMPILED_MODULES === '1';
|
||||
}
|
||||
|
||||
export function maybeStartPreparingPrecompiledModules(
|
||||
ctx: BuildContext,
|
||||
config: { precompiledModulesUrls: string[] }
|
||||
): void {
|
||||
if (!shouldUsePrecompiledDependencies(ctx.env)) {
|
||||
return;
|
||||
}
|
||||
|
||||
startPreparingPrecompiledDependencies(ctx, config.precompiledModulesUrls);
|
||||
}
|
||||
|
||||
export function startPreparingPrecompiledDependencies(ctx: BuildContext, urls: string[]): void {
|
||||
precompiledModulesPreparationPromise = preparePrecompiledDependenciesAsync({
|
||||
logger: ctx.logger,
|
||||
urls,
|
||||
destinationDirectory: PRECOMPILED_MODULES_PATH,
|
||||
cocoapodsProxyUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForPrecompiledModulesPreparationAsync(): Promise<void> {
|
||||
if (!precompiledModulesPreparationPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
precompiledModulesPreparationPromise,
|
||||
new Promise<void>((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out waiting for precompiled dependencies after ${PRECOMPILED_MODULES_WAIT_TIMEOUT_MS / 1000} seconds`
|
||||
)
|
||||
);
|
||||
}, PRECOMPILED_MODULES_WAIT_TIMEOUT_MS);
|
||||
timeoutHandle.unref?.();
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function preparePrecompiledDependenciesAsync({
|
||||
logger,
|
||||
urls,
|
||||
destinationDirectory,
|
||||
cocoapodsProxyUrl,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
urls: string[];
|
||||
destinationDirectory: string;
|
||||
cocoapodsProxyUrl?: string;
|
||||
}): Promise<void> {
|
||||
const archiveDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'precompiled-modules-'));
|
||||
const destinationParentDirectory = path.dirname(destinationDirectory);
|
||||
await fs.mkdirp(destinationParentDirectory);
|
||||
const stagingDirectory = await fs.mkdtemp(
|
||||
path.join(destinationParentDirectory, `${path.basename(destinationDirectory)}-staging-`)
|
||||
);
|
||||
|
||||
logger.info('');
|
||||
logger.info(
|
||||
{
|
||||
destinationDirectory,
|
||||
urls,
|
||||
},
|
||||
'Starting precompiled dependencies download'
|
||||
);
|
||||
|
||||
await fs.remove(destinationDirectory);
|
||||
await fs.mkdirp(destinationDirectory);
|
||||
|
||||
try {
|
||||
const archives = urls.map((url, index) => ({
|
||||
archiveName: path.basename(new URL(url).pathname),
|
||||
url,
|
||||
archivePath: path.join(archiveDirectory, `precompiled-modules-${index}.zip`),
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
archives.map(async ({ url, archivePath }) => {
|
||||
await downloadPrecompiledModulesAsync({
|
||||
url,
|
||||
archivePath,
|
||||
cocoapodsProxyUrl,
|
||||
logger,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
for (const { archiveName, url, archivePath } of archives) {
|
||||
logger.info({ archivePath, url }, `Downloaded ${archiveName}, extracting archive`);
|
||||
await extractZipAsync(archivePath, stagingDirectory);
|
||||
logger.info(
|
||||
{ destinationDirectory: stagingDirectory },
|
||||
`Extracted ${archiveName} into staging precompiled dependencies directory`
|
||||
);
|
||||
}
|
||||
|
||||
await fs.remove(destinationDirectory);
|
||||
await fs.move(stagingDirectory, destinationDirectory);
|
||||
logger.info({ destinationDirectory }, `Precompiled modules ready in ${destinationDirectory}`);
|
||||
} catch (error) {
|
||||
await fs.remove(stagingDirectory);
|
||||
await fs.remove(destinationDirectory);
|
||||
await fs.mkdirp(destinationDirectory);
|
||||
throw error;
|
||||
} finally {
|
||||
await fs.remove(archiveDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPrecompiledModulesAsync({
|
||||
url,
|
||||
archivePath,
|
||||
cocoapodsProxyUrl,
|
||||
logger,
|
||||
}: {
|
||||
url: string;
|
||||
archivePath: string;
|
||||
cocoapodsProxyUrl?: string;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
const proxiedUrl = cocoapodsProxyUrl
|
||||
? (() => {
|
||||
const parsedUrl = new URL(url);
|
||||
return `${cocoapodsProxyUrl}/${parsedUrl.hostname}${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
||||
})()
|
||||
: null;
|
||||
if (proxiedUrl) {
|
||||
try {
|
||||
await downloadFile(proxiedUrl, archivePath, { retry: 3 });
|
||||
return;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err, proxiedUrl },
|
||||
'Failed to download through CocoaPods proxy, retrying directly'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await downloadFile(url, archivePath, { retry: 3 });
|
||||
}
|
||||
|
||||
async function extractZipAsync(archivePath: string, destinationDirectory: string): Promise<void> {
|
||||
const zip = new StreamZip.async({ file: archivePath });
|
||||
try {
|
||||
await zip.extract(null, destinationDirectory);
|
||||
} finally {
|
||||
await zip.close();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.5.0"
|
||||
"@expo/steps": "18.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/downloader",
|
||||
"version": "18.5.0",
|
||||
"version": "18.0.1",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-build-cache-provider",
|
||||
"version": "18.5.0",
|
||||
"version": "18.0.2",
|
||||
"description": "A build cache provider plugin for the Expo CLI",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"joi": "^17.13.1",
|
||||
"semver": "^7.6.2",
|
||||
"zod": "^4.3.5"
|
||||
|
||||
@@ -24,98 +24,6 @@ describe('StaticWorkflowInterpolationContextZ', () => {
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect context', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
name: 'workflow-name',
|
||||
filename: 'workflow.yml',
|
||||
url: 'https://expo.dev/accounts/example/workflows/workflow-id',
|
||||
},
|
||||
app: {
|
||||
id: 'app-id',
|
||||
slug: 'app-slug',
|
||||
},
|
||||
account: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
},
|
||||
app_store_connect: {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
build_upload: {
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
state: 'processing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('accepts app_store_connect context without build_upload', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
name: 'workflow-name',
|
||||
filename: 'workflow.yml',
|
||||
url: 'https://expo.dev/accounts/example/workflows/workflow-id',
|
||||
},
|
||||
app: {
|
||||
id: 'app-id',
|
||||
slug: 'app-slug',
|
||||
},
|
||||
account: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
},
|
||||
app_store_connect: {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(StaticWorkflowInterpolationContextZ.parse(context)).toEqual(context);
|
||||
});
|
||||
|
||||
it('rejects invalid app_store_connect build_upload context', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
needs: {},
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
name: 'workflow-name',
|
||||
filename: 'workflow.yml',
|
||||
url: 'https://expo.dev/accounts/example/workflows/workflow-id',
|
||||
},
|
||||
app: {
|
||||
id: 'app-id',
|
||||
slug: 'app-slug',
|
||||
},
|
||||
account: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
},
|
||||
app_store_connect: {
|
||||
app: {
|
||||
id: '1234567890',
|
||||
},
|
||||
build_upload: {
|
||||
id: 'not-a-uuid',
|
||||
state: 'invalid-state',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => StaticWorkflowInterpolationContextZ.parse(context)).toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid app and account context', () => {
|
||||
const context = {
|
||||
after: {},
|
||||
|
||||
@@ -259,19 +259,6 @@ export const StaticWorkflowInterpolationContextZ = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
app_store_connect: z
|
||||
.looseObject({
|
||||
app: z.looseObject({
|
||||
id: z.string(),
|
||||
}),
|
||||
build_upload: z
|
||||
.looseObject({
|
||||
id: z.string(),
|
||||
state: z.enum(['awaiting_upload', 'processing', 'failed', 'complete']),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type StaticWorkflowInterpolationContext = z.infer<
|
||||
|
||||
+90
-92
@@ -178,7 +178,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -195,7 +195,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -217,7 +217,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -234,7 +234,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -248,7 +248,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -300,7 +300,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -321,7 +321,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -341,7 +341,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -361,7 +361,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -384,7 +384,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -422,7 +422,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -442,7 +442,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -460,7 +460,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -480,7 +480,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -501,7 +501,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -522,7 +522,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -560,7 +560,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -600,7 +600,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -630,7 +630,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -656,7 +656,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -708,7 +708,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -726,7 +726,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -744,7 +744,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -761,7 +761,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -782,7 +782,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -803,7 +803,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -825,7 +825,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -845,7 +845,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -867,7 +867,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -889,7 +889,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -924,7 +924,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -947,7 +947,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -968,7 +968,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -985,7 +985,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1003,7 +1003,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1032,7 +1032,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1057,7 +1057,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1078,7 +1078,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1099,7 +1099,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1136,7 +1136,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1156,7 +1156,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1177,7 +1177,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1198,7 +1198,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1212,7 +1212,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1226,7 +1226,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1258,7 +1258,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1284,7 +1284,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1306,7 +1306,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1334,7 +1334,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1362,7 +1362,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1385,7 +1385,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1407,7 +1407,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1441,7 +1441,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1485,7 +1485,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1516,7 +1516,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1625,7 +1625,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1633,17 +1633,16 @@ generate the local store configuration from the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:pull [-e <value>] [--non-interactive]
|
||||
$ eas metadata:pull [-e <value>]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1651,17 +1650,16 @@ sync the local store configuration to the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:push [-e <value>] [--non-interactive]
|
||||
$ eas metadata:push [-e <value>]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1713,7 +1711,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1727,7 +1725,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1750,7 +1748,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1774,7 +1772,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1792,7 +1790,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1825,7 +1823,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1868,7 +1866,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1889,7 +1887,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1910,7 +1908,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1935,7 +1933,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1962,7 +1960,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2000,7 +1998,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2028,7 +2026,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2058,7 +2056,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2079,7 +2077,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2099,7 +2097,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2120,7 +2118,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2142,7 +2140,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2162,7 +2160,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2181,7 +2179,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2204,7 +2202,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2221,7 +2219,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2342,7 +2340,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2362,7 +2360,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2386,7 +2384,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2426,7 +2424,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2448,7 +2446,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2476,7 +2474,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2496,7 +2494,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2518,5 +2516,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.5.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.4.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -43,15 +43,15 @@
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.16",
|
||||
"@expo/apple-utils": "2.1.15",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/eas-json": "18.5.0",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/eas-json": "18.4.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/multipart-body-parser": "2.0.0",
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
|
||||
@@ -315,42 +315,6 @@
|
||||
"liveEdits": true,
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"type": "object",
|
||||
"description": "Screenshots for this locale, organized by display type (e.g., APP_IPHONE_67, APP_IPAD_PRO_129)",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"previews": {
|
||||
"type": "object",
|
||||
"description": "Video previews for this locale, organized by display type (e.g., IPHONE_67, IPAD_PRO_129)",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Video file path (relative to project root)"
|
||||
},
|
||||
"previewFrameTimeCode": {
|
||||
"type": "string",
|
||||
"description": "Optional preview frame time code (e.g., '00:05:00' for 5 seconds)"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@ async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): P
|
||||
},
|
||||
{
|
||||
configPath: 'exp.notification.icon',
|
||||
pngPath: (ctx.exp as Record<string, any>).notification?.icon,
|
||||
pngPath: ctx.exp.notification?.icon,
|
||||
},
|
||||
];
|
||||
await validatePNGsAsync(pngs);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import uniq from '../../../utils/expodash/uniq';
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { removeDatePrecision } from '../../utils/date';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
|
||||
|
||||
@@ -211,14 +211,4 @@ export class AppleConfigReader {
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
|
||||
/** Get screenshots configuration for a specific locale */
|
||||
public getScreenshots(locale: string): AppleScreenshots | null {
|
||||
return this.schema.info?.[locale]?.screenshots ?? null;
|
||||
}
|
||||
|
||||
/** Get video previews configuration for a specific locale */
|
||||
public getPreviews(locale: string): ApplePreviews | null {
|
||||
return this.schema.info?.[locale]?.previews ?? null;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
/**
|
||||
* Serializes the Apple ASC entities into the metadata configuration schema.
|
||||
@@ -186,21 +186,6 @@ export class AppleConfigWriter {
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
|
||||
/** Set screenshots for a specific locale */
|
||||
public setScreenshots(locale: string, screenshots: AppleScreenshots): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
|
||||
this.schema.info[locale].screenshots =
|
||||
Object.keys(screenshots).length > 0 ? screenshots : undefined;
|
||||
}
|
||||
|
||||
/** Set video previews for a specific locale */
|
||||
public setPreviews(locale: string, previews: ApplePreviews): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
|
||||
this.schema.info[locale].previews = Object.keys(previews).length > 0 ? previews : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to convert `T | null` to `T | undefined`, required for the entity properties */
|
||||
|
||||
@@ -4,25 +4,16 @@ import type { AgeRatingData } from './tasks/age-rating';
|
||||
import type { AppInfoData } from './tasks/app-info';
|
||||
import type { AppReviewData } from './tasks/app-review-detail';
|
||||
import type { AppVersionData } from './tasks/app-version';
|
||||
import type { PreviewsData } from './tasks/previews';
|
||||
import type { ScreenshotsData } from './tasks/screenshots';
|
||||
|
||||
/**
|
||||
* The fully prepared apple data, used within the `downloadAsync` or `uploadAsync` tasks.
|
||||
* It contains references to each individual models, to either upload or download App Store data.
|
||||
*/
|
||||
export type AppleData = { app: App; projectDir: string } & AppInfoData &
|
||||
AppVersionData &
|
||||
AgeRatingData &
|
||||
AppReviewData &
|
||||
ScreenshotsData &
|
||||
PreviewsData;
|
||||
export type AppleData = { app: App } & AppInfoData & AppVersionData & AgeRatingData & AppReviewData;
|
||||
|
||||
/**
|
||||
* The unprepared partial apple data, used within the `prepareAsync` tasks.
|
||||
* It contains a reference to the app, each task should populate the necessary data.
|
||||
* If an entity fails to prepare the data, individual tasks should raise errors about the missing data.
|
||||
*/
|
||||
export type PartialAppleData = { app: App; projectDir: string } & Partial<
|
||||
Omit<AppleData, 'app' | 'projectDir'>
|
||||
>;
|
||||
export type PartialAppleData = { app: App } & Partial<Omit<AppleData, 'app'>>;
|
||||
@@ -1,14 +1,8 @@
|
||||
import { truthy } from '../../../utils/expodash/filter';
|
||||
import { IssueRule } from '../../config/issue';
|
||||
import { AppleInfo } from '../types';
|
||||
|
||||
/** Only check text properties that may contain restricted words */
|
||||
type AppleInfoTextProperty = 'title' | 'subtitle' | 'description' | 'keywords';
|
||||
const RESTRICTED_PROPERTIES: AppleInfoTextProperty[] = [
|
||||
'title',
|
||||
'subtitle',
|
||||
'description',
|
||||
'keywords',
|
||||
];
|
||||
const RESTRICTED_PROPERTIES: (keyof AppleInfo)[] = ['title', 'subtitle', 'description', 'keywords'];
|
||||
const RESTRICTED_WORDS = {
|
||||
beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AgeRatingDeclaration, AppInfo, Rating } from '@expo/apple-utils';
|
||||
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
@@ -12,13 +12,13 @@ jest.mock('../../config/writer');
|
||||
|
||||
describe(AgeRatingTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('loads age rating from app info', async () => {
|
||||
it('loads age rating from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppInfo.type}/stub-id/ageRatingDeclaration`)
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
|
||||
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json'));
|
||||
|
||||
const context: any = {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AgeRatingTask().prepareAsync({ context });
|
||||
@@ -26,16 +26,6 @@ describe(AgeRatingTask, () => {
|
||||
expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration);
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('skips when info is not available', async () => {
|
||||
const context: any = {
|
||||
info: undefined,
|
||||
};
|
||||
|
||||
await new AgeRatingTask().prepareAsync({ context });
|
||||
|
||||
expect(context.ageRating).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
@@ -64,13 +54,13 @@ describe(AgeRatingTask, () => {
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when age rating is not loaded', async () => {
|
||||
await expect(
|
||||
new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { ageRating: undefined } as any,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
it('aborts when age rating is not loaded', async () => {
|
||||
const promise = new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { ageRating: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('rating not initialized');
|
||||
});
|
||||
|
||||
it('skips updating age rating when not configured', async () => {
|
||||
|
||||
@@ -23,7 +23,6 @@ describe(AppInfoTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppInfoTask().prepareAsync({ context });
|
||||
|
||||
@@ -72,20 +72,20 @@ describe(AppReviewDetailTask, () => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('skips when version is not loaded', async () => {
|
||||
await expect(
|
||||
new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: '[email protected]',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {} as any,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: 'review@example.com',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {} as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('version not init');
|
||||
});
|
||||
|
||||
it('updates review details when loaded', async () => {
|
||||
|
||||
@@ -44,7 +44,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
@@ -84,7 +83,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
@@ -120,7 +118,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
@@ -156,7 +153,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
@@ -198,7 +194,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ version: '2.0' }).prepareAsync({ context });
|
||||
@@ -248,7 +243,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ version: '3.0' }).prepareAsync({ context });
|
||||
@@ -377,7 +371,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
};
|
||||
|
||||
@@ -406,7 +399,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
versionPhasedRelease: null, // Not enabled yet
|
||||
};
|
||||
@@ -425,7 +417,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
// Enabled, and not completed yet
|
||||
versionPhasedRelease: new AppStoreVersionPhasedRelease(
|
||||
@@ -449,9 +440,8 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
// Enabled, and completed
|
||||
// Enabled, and not completed yet
|
||||
versionPhasedRelease: new AppStoreVersionPhasedRelease(
|
||||
requestContext,
|
||||
'APP_STORE_VERSION_PHASED_RELEASE_1',
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
import {
|
||||
AppPreview,
|
||||
AppPreviewSet,
|
||||
AppStoreVersionLocalization,
|
||||
PreviewType,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { PartialAppleData } from '../../data';
|
||||
import { PreviewsTask } from '../previews';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
const mockFetch = jest.fn();
|
||||
jest.mock('../../../../fetch', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: any[]) => mockFetch(...args),
|
||||
}));
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
jest.spyOn(fs, 'statSync').mockReturnValue({ size: 10240 } as any);
|
||||
jest.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
|
||||
jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
describe(PreviewsTask, () => {
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('prepareAsync', () => {
|
||||
it('initializes empty preview sets when no locales are available', async () => {
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: undefined,
|
||||
};
|
||||
|
||||
await new PreviewsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.previewSets).toBeDefined();
|
||||
expect(context.previewSets!.size).toBe(0);
|
||||
});
|
||||
|
||||
it('fetches preview sets for each locale', async () => {
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersionLocalization.type}/LOC_1/${AppPreviewSet.type}`)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: [locale],
|
||||
};
|
||||
|
||||
await new PreviewsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.previewSets).toBeDefined();
|
||||
expect(context.previewSets!.size).toBe(1);
|
||||
expect(context.previewSets!.get('en-US')).toBeDefined();
|
||||
expect(context.previewSets!.get('en-US')!.has(PreviewType.IPHONE_67)).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips when preview sets are not prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { previewSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('skips locales with no preview sets', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('downloads previews and sets config with simple path', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const preview = new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
videoUrl: 'https://example.com/demo.mp4',
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(preview, 'getVideoUrl').mockReturnValue('https://example.com/demo.mp4');
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
const previewSet = new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [preview],
|
||||
} as any);
|
||||
previewTypeMap.set(PreviewType.IPHONE_67, previewSet);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-video-data')),
|
||||
});
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: 'store/apple/preview/en-US/IPHONE_67/demo.mp4',
|
||||
});
|
||||
});
|
||||
|
||||
it('downloads previews with previewFrameTimeCode as object config', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const preview = new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
videoUrl: 'https://example.com/demo.mp4',
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(preview, 'getVideoUrl').mockReturnValue('https://example.com/demo.mp4');
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
const previewSet = new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [preview],
|
||||
} as any);
|
||||
previewTypeMap.set(PreviewType.IPHONE_67, previewSet);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-video-data')),
|
||||
});
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: {
|
||||
path: 'store/apple/preview/en-US/IPHONE_67/demo.mp4',
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when preview sets are not prepared', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: { previewSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips when no locales are configured', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets: new Map(),
|
||||
versionLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips locales without previews in config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
// No previews
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets: new Map(),
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should complete without uploading
|
||||
});
|
||||
|
||||
it('uploads new preview for configured locale with string path', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: './previews/demo.mp4',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newPreview = new AppPreview(requestContext, 'NEW_PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create preview set
|
||||
.post(`/v1/${AppPreviewSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppPreview.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_PSET_1',
|
||||
filePath: '/test/project/previews/demo.mp4',
|
||||
waitForProcessing: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uploads new preview with object config including previewFrameTimeCode', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: {
|
||||
path: './previews/demo.mp4',
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newPreview = new AppPreview(requestContext, 'NEW_PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create preview set
|
||||
.post(`/v1/${AppPreviewSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppPreview.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_PSET_1',
|
||||
filePath: '/test/project/previews/demo.mp4',
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,303 +0,0 @@
|
||||
import {
|
||||
AppScreenshot,
|
||||
AppScreenshotSet,
|
||||
AppStoreVersionLocalization,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { PartialAppleData } from '../../data';
|
||||
import { ScreenshotsTask } from '../screenshots';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
const mockFetch = jest.fn();
|
||||
jest.mock('../../../../fetch', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: any[]) => mockFetch(...args),
|
||||
}));
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
jest.spyOn(fs, 'statSync').mockReturnValue({ size: 1024 } as any);
|
||||
jest.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
|
||||
jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
describe(ScreenshotsTask, () => {
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('prepareAsync', () => {
|
||||
it('initializes empty screenshot sets when no locales are available', async () => {
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: undefined,
|
||||
};
|
||||
|
||||
await new ScreenshotsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.screenshotSets).toBeDefined();
|
||||
expect(context.screenshotSets!.size).toBe(0);
|
||||
});
|
||||
|
||||
it('fetches screenshot sets for each locale', async () => {
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersionLocalization.type}/LOC_1/${AppScreenshotSet.type}`)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'SET_1',
|
||||
type: AppScreenshotSet.type,
|
||||
attributes: {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: [locale],
|
||||
};
|
||||
|
||||
await new ScreenshotsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.screenshotSets).toBeDefined();
|
||||
expect(context.screenshotSets!.size).toBe(1);
|
||||
expect(context.screenshotSets!.get('en-US')).toBeDefined();
|
||||
expect(
|
||||
context.screenshotSets!.get('en-US')!.has(ScreenshotDisplayType.APP_IPHONE_67)
|
||||
).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips when screenshot sets are not prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { screenshotSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('skips locales with no screenshot sets', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', new Map());
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('downloads screenshots and sets config', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const screenshot = new AppScreenshot(requestContext, 'SS_1', {
|
||||
fileName: 'home.png',
|
||||
fileSize: 1024,
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Mock getImageAssetUrl
|
||||
jest.spyOn(screenshot, 'getImageAssetUrl').mockReturnValue('https://example.com/home.png');
|
||||
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
const screenshotSet = new AppScreenshotSet(requestContext, 'SET_1', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [screenshot],
|
||||
} as any);
|
||||
displayTypeMap.set(ScreenshotDisplayType.APP_IPHONE_67, screenshotSet);
|
||||
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', displayTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
// Mock fetch response
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
|
||||
});
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).toBeCalledWith('en-US', {
|
||||
[ScreenshotDisplayType.APP_IPHONE_67]: [
|
||||
'store/apple/screenshot/en-US/APP_IPHONE_67/home.png',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when screenshot sets are not prepared', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: { screenshotSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips when no locales are configured', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets: new Map(),
|
||||
versionLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips locales without screenshots in config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
// No screenshots
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets: new Map(),
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should complete without uploading
|
||||
});
|
||||
|
||||
it('uploads new screenshots for configured locale', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
screenshots: {
|
||||
APP_IPHONE_67: ['./screenshots/home.png'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newScreenshot = new AppScreenshot(requestContext, 'NEW_SS_1', {
|
||||
fileName: 'home.png',
|
||||
fileSize: 1024,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
// Mock createAppScreenshotSetAsync on the locale
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create screenshot set
|
||||
.post(`/v1/${AppScreenshotSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_SET_1',
|
||||
type: AppScreenshotSet.type,
|
||||
attributes: {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppScreenshot.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppScreenshot, 'uploadAsync').mockResolvedValue(newScreenshot);
|
||||
|
||||
// Mock AppScreenshotSet.infoAsync for reorder step
|
||||
jest.spyOn(AppScreenshotSet, 'infoAsync').mockResolvedValue(
|
||||
new AppScreenshotSet(requestContext, 'NEW_SET_1', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [newScreenshot],
|
||||
} as any)
|
||||
);
|
||||
|
||||
// Mock reorderScreenshotsAsync
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppScreenshotSet.prototype.reorderScreenshotsAsync = reorderMock;
|
||||
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', new Map());
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppScreenshot.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_SET_1',
|
||||
filePath: '/test/project/screenshots/home.png',
|
||||
waitForProcessing: true,
|
||||
})
|
||||
);
|
||||
// Reorder is skipped because the current order already matches
|
||||
expect(reorderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AgeRatingDeclaration } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
@@ -6,7 +7,7 @@ import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AgeRatingData = {
|
||||
/** The app age rating declaration for the app info */
|
||||
/** The app age rating declaration for the app version */
|
||||
ageRating: AgeRatingDeclaration;
|
||||
};
|
||||
|
||||
@@ -14,22 +15,7 @@ export class AgeRatingTask extends AppleTask {
|
||||
public name = (): string => 'age rating declarations';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
if (!context.info) {
|
||||
return;
|
||||
}
|
||||
// The ageRatingDeclaration relationship is on appInfos (not appStoreVersions).
|
||||
try {
|
||||
context.ageRating = (await context.info.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
} catch (error: any) {
|
||||
// Gracefully handle cases where the relationship is not available
|
||||
if (error?.message?.includes('ageRatingDeclaration')) {
|
||||
Log.warn(
|
||||
chalk`{yellow Skipped age rating - not available for this app. This may require updating through App Store Connect directly.}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
context.ageRating = (await context.version!.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
@@ -39,10 +25,7 @@ export class AgeRatingTask extends AppleTask {
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.ageRating) {
|
||||
Log.log(chalk`{dim - Skipped age rating update, not available}`);
|
||||
return;
|
||||
}
|
||||
assert(context.ageRating, `Age rating not initialized, can't update age rating`);
|
||||
|
||||
const ageRating = config.getAgeRating();
|
||||
if (!ageRating) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppStoreReviewDetail } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
@@ -15,10 +16,7 @@ export class AppReviewDetailTask extends AppleTask {
|
||||
public name = (): string => 'app review detail';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
if (!context.version) {
|
||||
return;
|
||||
}
|
||||
context.reviewDetail = (await context.version.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
context.reviewDetail = (await context.version!.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
@@ -34,10 +32,7 @@ export class AppReviewDetailTask extends AppleTask {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.version) {
|
||||
Log.log(chalk`{dim - Skipped store review details, no version available}`);
|
||||
return;
|
||||
}
|
||||
assert(context.version, `App version not initialized, can't upload store review details`);
|
||||
const { versionString } = context.version.attributes;
|
||||
|
||||
if (!context.reviewDetail) {
|
||||
|
||||
@@ -2,8 +2,6 @@ import { AgeRatingTask } from './age-rating';
|
||||
import { AppInfoTask } from './app-info';
|
||||
import { AppReviewDetailTask } from './app-review-detail';
|
||||
import { AppVersionOptions, AppVersionTask } from './app-version';
|
||||
import { PreviewsTask } from './previews';
|
||||
import { ScreenshotsTask } from './screenshots';
|
||||
import { AppleTask } from '../task';
|
||||
|
||||
type AppleTaskOptions = {
|
||||
@@ -19,7 +17,5 @@ export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[
|
||||
new AppInfoTask(),
|
||||
new AgeRatingTask(),
|
||||
new AppReviewDetailTask(),
|
||||
new ScreenshotsTask(),
|
||||
new PreviewsTask(),
|
||||
];
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
import {
|
||||
AppPreview,
|
||||
AppPreviewSet,
|
||||
AppStoreVersionLocalization,
|
||||
PreviewType,
|
||||
} from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import fetch from '../../../fetch';
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
import { ApplePreviewConfig, ApplePreviews } from '../types';
|
||||
|
||||
/** Locale -> PreviewType -> AppPreviewSet */
|
||||
export type PreviewSetsMap = Map<string, Map<PreviewType, AppPreviewSet>>;
|
||||
|
||||
export type PreviewsData = {
|
||||
/** Map of locales to their preview sets */
|
||||
previewSets: PreviewSetsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize preview config to always return an object with path and optional previewFrameTimeCode.
|
||||
*/
|
||||
function normalizePreviewConfig(config: ApplePreviewConfig): {
|
||||
path: string;
|
||||
previewFrameTimeCode?: string;
|
||||
} {
|
||||
if (typeof config === 'string') {
|
||||
return { path: config };
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for managing App Store video previews.
|
||||
* Downloads existing previews and uploads new ones based on store configuration.
|
||||
*/
|
||||
export class PreviewsTask extends AppleTask {
|
||||
public name = (): string => 'video previews';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
// Initialize the preview sets map
|
||||
context.previewSets = new Map();
|
||||
|
||||
if (!context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch preview sets for all locales in parallel
|
||||
await Promise.all(
|
||||
context.versionLocales.map(async locale => {
|
||||
const sets = await locale.getAppPreviewSetsAsync();
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
|
||||
for (const set of sets) {
|
||||
previewTypeMap.set(set.attributes.previewType, set);
|
||||
}
|
||||
|
||||
context.previewSets!.set(locale.attributes.locale, previewTypeMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (!context.previewSets || !context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
const localeCode = locale.attributes.locale;
|
||||
const previewTypeMap = context.previewSets.get(localeCode);
|
||||
|
||||
if (!previewTypeMap || previewTypeMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previews: ApplePreviews = {};
|
||||
|
||||
for (const [previewType, set] of previewTypeMap) {
|
||||
const previewModels = set.attributes.appPreviews;
|
||||
if (!previewModels || previewModels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For now, we only handle the first preview per set (App Store allows up to 3)
|
||||
// We can extend this later to support multiple previews
|
||||
const preview = previewModels[0];
|
||||
const relativePath = await downloadPreviewAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
previewType,
|
||||
preview,
|
||||
0
|
||||
);
|
||||
|
||||
if (relativePath) {
|
||||
// Include preview frame time code if available
|
||||
if (preview.attributes.previewFrameTimeCode) {
|
||||
previews[previewType] = {
|
||||
path: relativePath,
|
||||
previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
|
||||
};
|
||||
} else {
|
||||
previews[previewType] = relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(previews).length > 0) {
|
||||
config.setPreviews(localeCode, previews);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.previewSets || !context.versionLocales) {
|
||||
Log.log(chalk`{dim - Skipped video previews, no version available}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped video previews, no locales configured}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const localeCode of locales) {
|
||||
const previews = config.getPreviews(localeCode);
|
||||
if (!previews || Object.keys(previews).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
||||
if (!localization) {
|
||||
Log.warn(chalk`{yellow Skipping video previews for ${localeCode} - locale not found}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
||||
if (!previewConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await syncPreviewSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
previewType as PreviewType,
|
||||
normalizePreviewConfig(previewConfig),
|
||||
context.previewSets.get(localeCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a preview set - upload new preview, delete old one if changed.
|
||||
*/
|
||||
async function syncPreviewSetAsync(
|
||||
projectDir: string,
|
||||
localization: AppStoreVersionLocalization,
|
||||
previewType: PreviewType,
|
||||
previewConfig: { path: string; previewFrameTimeCode?: string },
|
||||
existingSets: Map<PreviewType, AppPreviewSet> | undefined
|
||||
): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
const absolutePath = path.resolve(projectDir, previewConfig.path);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow Video preview not found: ${absolutePath}}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create the preview set
|
||||
let previewSet = existingSets?.get(previewType);
|
||||
|
||||
if (!previewSet) {
|
||||
previewSet = await logAsync(
|
||||
() =>
|
||||
localization.createAppPreviewSetAsync({
|
||||
previewType,
|
||||
}),
|
||||
{
|
||||
pending: `Creating preview set for ${chalk.bold(previewType)} (${locale})...`,
|
||||
success: `Created preview set for ${chalk.bold(previewType)} (${locale})`,
|
||||
failure: `Failed creating preview set for ${chalk.bold(previewType)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const existingPreviews = previewSet.attributes.appPreviews || [];
|
||||
|
||||
// Check if we need to update (different filename, size, or no existing preview)
|
||||
const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
|
||||
const localSize = fs.statSync(absolutePath).size;
|
||||
|
||||
if (
|
||||
existingPreview &&
|
||||
existingPreview.isComplete() &&
|
||||
existingPreview.attributes.fileSize === localSize
|
||||
) {
|
||||
// Preview with same filename exists, check if we need to update preview frame time code
|
||||
if (
|
||||
previewConfig.previewFrameTimeCode &&
|
||||
existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode
|
||||
) {
|
||||
await logAsync(
|
||||
() =>
|
||||
existingPreview.updateAsync({
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Updating preview frame time code for ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Updated preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed updating preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
Log.log(chalk`{dim Preview ${fileName} already exists, skipping upload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing previews that don't match
|
||||
for (const preview of existingPreviews) {
|
||||
if (preview.attributes.fileName !== fileName) {
|
||||
await logAsync(() => preview.deleteAsync(), {
|
||||
pending: `Deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new preview
|
||||
await logAsync(
|
||||
() =>
|
||||
AppPreview.uploadAsync(localization.context, {
|
||||
id: previewSet!.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading video preview ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a video preview to the local filesystem.
|
||||
* Returns the relative path to the downloaded file.
|
||||
*/
|
||||
async function downloadPreviewAsync(
|
||||
projectDir: string,
|
||||
locale: string,
|
||||
previewType: PreviewType,
|
||||
preview: AppPreview,
|
||||
index: number
|
||||
): Promise<string | null> {
|
||||
const videoUrl = preview.getVideoUrl();
|
||||
if (!videoUrl) {
|
||||
Log.warn(chalk`{yellow Could not get download URL for preview ${preview.attributes.fileName}}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create directory structure: store/apple/preview/{locale}/{previewType}/
|
||||
const previewsDir = path.join(projectDir, 'store', 'apple', 'preview', locale, previewType);
|
||||
await fs.promises.mkdir(previewsDir, { recursive: true });
|
||||
|
||||
// Use original filename for matching during sync
|
||||
const fileName = preview.attributes.fileName || `${String(index + 1).padStart(2, '0')}.mp4`;
|
||||
const outputPath = path.join(previewsDir, fileName);
|
||||
const relativePath = path.relative(projectDir, outputPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(videoUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
Log.log(chalk`{dim Downloaded video preview: ${relativePath}}`);
|
||||
return relativePath;
|
||||
} catch (error: any) {
|
||||
Log.warn(chalk`{yellow Failed to download video preview ${fileName}: ${error.message}}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
import {
|
||||
AppScreenshot,
|
||||
AppScreenshotSet,
|
||||
AppStoreVersionLocalization,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import fetch from '../../../fetch';
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
import { AppleScreenshots } from '../types';
|
||||
|
||||
/** Locale -> ScreenshotDisplayType -> AppScreenshotSet */
|
||||
export type ScreenshotSetsMap = Map<string, Map<ScreenshotDisplayType, AppScreenshotSet>>;
|
||||
|
||||
export type ScreenshotsData = {
|
||||
/** Map of locales to their screenshot sets */
|
||||
screenshotSets: ScreenshotSetsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Task for managing App Store screenshots.
|
||||
* Downloads existing screenshots and uploads new ones based on store configuration.
|
||||
*/
|
||||
export class ScreenshotsTask extends AppleTask {
|
||||
public name = (): string => 'screenshots';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
// Initialize the screenshot sets map
|
||||
context.screenshotSets = new Map();
|
||||
|
||||
if (!context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch screenshot sets for all locales in parallel
|
||||
await Promise.all(
|
||||
context.versionLocales.map(async locale => {
|
||||
const sets = await locale.getAppScreenshotSetsAsync();
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
|
||||
for (const set of sets) {
|
||||
displayTypeMap.set(set.attributes.screenshotDisplayType, set);
|
||||
}
|
||||
|
||||
context.screenshotSets!.set(locale.attributes.locale, displayTypeMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (!context.screenshotSets || !context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
const localeCode = locale.attributes.locale;
|
||||
const displayTypeMap = context.screenshotSets.get(localeCode);
|
||||
|
||||
if (!displayTypeMap || displayTypeMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshots: AppleScreenshots = {};
|
||||
|
||||
for (const [displayType, set] of displayTypeMap) {
|
||||
const screenshotModels = set.attributes.appScreenshots;
|
||||
if (!screenshotModels || screenshotModels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download screenshots and save to local filesystem
|
||||
const paths: string[] = [];
|
||||
for (let i = 0; i < screenshotModels.length; i++) {
|
||||
const screenshot = screenshotModels[i];
|
||||
const relativePath = await downloadScreenshotAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
displayType,
|
||||
screenshot,
|
||||
i
|
||||
);
|
||||
if (relativePath) {
|
||||
paths.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (paths.length > 0) {
|
||||
screenshots[displayType] = paths;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(screenshots).length > 0) {
|
||||
config.setScreenshots(localeCode, screenshots);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.screenshotSets || !context.versionLocales) {
|
||||
Log.log(chalk`{dim - Skipped screenshots, no version available}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped screenshots, no locales configured}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const localeCode of locales) {
|
||||
const screenshots = config.getScreenshots(localeCode);
|
||||
if (!screenshots || Object.keys(screenshots).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
||||
if (!localization) {
|
||||
Log.warn(chalk`{yellow Skipping screenshots for ${localeCode} - locale not found}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [displayType, paths] of Object.entries(screenshots)) {
|
||||
if (!paths || paths.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await syncScreenshotSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
displayType as ScreenshotDisplayType,
|
||||
paths,
|
||||
context.screenshotSets.get(localeCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a screenshot set - upload new screenshots, delete removed ones, reorder if needed.
|
||||
*/
|
||||
async function syncScreenshotSetAsync(
|
||||
projectDir: string,
|
||||
localization: AppStoreVersionLocalization,
|
||||
displayType: ScreenshotDisplayType,
|
||||
paths: string[],
|
||||
existingSets: Map<ScreenshotDisplayType, AppScreenshotSet> | undefined
|
||||
): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
|
||||
// Get or create the screenshot set
|
||||
let screenshotSet = existingSets?.get(displayType);
|
||||
|
||||
if (!screenshotSet) {
|
||||
screenshotSet = await logAsync(
|
||||
() =>
|
||||
localization.createAppScreenshotSetAsync({
|
||||
screenshotDisplayType: displayType,
|
||||
}),
|
||||
{
|
||||
pending: `Creating screenshot set for ${chalk.bold(displayType)} (${locale})...`,
|
||||
success: `Created screenshot set for ${chalk.bold(displayType)} (${locale})`,
|
||||
failure: `Failed creating screenshot set for ${chalk.bold(displayType)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const existingScreenshots = screenshotSet.attributes.appScreenshots || [];
|
||||
|
||||
// Build a map of existing screenshots by filename for comparison
|
||||
const existingByFilename = new Map<string, AppScreenshot>();
|
||||
for (const screenshot of existingScreenshots) {
|
||||
existingByFilename.set(screenshot.attributes.fileName, screenshot);
|
||||
}
|
||||
|
||||
// Track which screenshots to keep, upload, and delete
|
||||
const screenshotIdsToKeep: string[] = [];
|
||||
const pathsToUpload: string[] = [];
|
||||
|
||||
for (const relativePath of paths) {
|
||||
const absolutePath = path.resolve(projectDir, relativePath);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
// Check if screenshot already exists with same name and file size
|
||||
const existing = existingByFilename.get(fileName);
|
||||
const localSize = fs.existsSync(absolutePath) ? fs.statSync(absolutePath).size : null;
|
||||
if (
|
||||
existing &&
|
||||
existing.isComplete() &&
|
||||
(localSize === null || existing.attributes.fileSize === localSize)
|
||||
) {
|
||||
screenshotIdsToKeep.push(existing.id);
|
||||
existingByFilename.delete(fileName);
|
||||
} else {
|
||||
pathsToUpload.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete screenshots that are no longer in config
|
||||
for (const screenshot of existingByFilename.values()) {
|
||||
await logAsync(() => screenshot.deleteAsync(), {
|
||||
pending: `Deleting screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
|
||||
// Upload new screenshots
|
||||
for (const absolutePath of pathsToUpload) {
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow Screenshot not found: ${absolutePath}}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newScreenshot = await logAsync(
|
||||
() =>
|
||||
AppScreenshot.uploadAsync(localization.context, {
|
||||
id: screenshotSet!.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading screenshot ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded screenshot ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading screenshot ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
|
||||
screenshotIdsToKeep.push(newScreenshot.id);
|
||||
}
|
||||
|
||||
// Reorder screenshots to match config order
|
||||
if (screenshotIdsToKeep.length > 0) {
|
||||
const refreshedSet = await AppScreenshotSet.infoAsync(localization.context, {
|
||||
id: screenshotSet.id,
|
||||
});
|
||||
const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
|
||||
const screenshotsByFilename = new Map<string, AppScreenshot>();
|
||||
for (const s of refreshedScreenshots) {
|
||||
screenshotsByFilename.set(s.attributes.fileName, s);
|
||||
}
|
||||
|
||||
// Build the desired order based on config paths
|
||||
const orderedIds: string[] = [];
|
||||
for (const relativePath of paths) {
|
||||
const fileName = path.basename(relativePath);
|
||||
const screenshot = screenshotsByFilename.get(fileName);
|
||||
if (screenshot) {
|
||||
orderedIds.push(screenshot.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Only call reorder if the order actually differs from current
|
||||
const currentIds = refreshedScreenshots.map(s => s.id);
|
||||
if (
|
||||
orderedIds.length > 0 &&
|
||||
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))
|
||||
) {
|
||||
await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a screenshot to the local filesystem.
|
||||
* Returns the relative path to the downloaded file.
|
||||
*/
|
||||
async function downloadScreenshotAsync(
|
||||
projectDir: string,
|
||||
locale: string,
|
||||
displayType: ScreenshotDisplayType,
|
||||
screenshot: AppScreenshot,
|
||||
index: number
|
||||
): Promise<string | null> {
|
||||
const imageUrl = screenshot.getImageAssetUrl({ type: 'png' });
|
||||
if (!imageUrl) {
|
||||
Log.warn(
|
||||
chalk`{yellow Could not get download URL for screenshot ${screenshot.attributes.fileName}}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create directory structure: store/apple/screenshot/{locale}/{displayType}/
|
||||
const screenshotsDir = path.join(projectDir, 'store', 'apple', 'screenshot', locale, displayType);
|
||||
await fs.promises.mkdir(screenshotsDir, { recursive: true });
|
||||
|
||||
// Use original filename for matching during sync
|
||||
const fileName = screenshot.attributes.fileName || `${String(index + 1).padStart(2, '0')}.png`;
|
||||
const outputPath = path.join(screenshotsDir, fileName);
|
||||
const relativePath = path.relative(projectDir, outputPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
Log.log(chalk`{dim Downloaded screenshot: ${relativePath}}`);
|
||||
return relativePath;
|
||||
} catch (error: any) {
|
||||
Log.warn(chalk`{yellow Failed to download screenshot ${fileName}: ${error.message}}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,7 @@
|
||||
import type {
|
||||
AgeRatingDeclarationProps,
|
||||
PreviewType,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
|
||||
|
||||
export type AppleLocale = string;
|
||||
|
||||
/** Screenshot display type enum values from App Store Connect API */
|
||||
export type AppleScreenshotDisplayType = `${ScreenshotDisplayType}`;
|
||||
|
||||
/** Preview display type enum values from App Store Connect API */
|
||||
export type ApplePreviewType = `${PreviewType}`;
|
||||
|
||||
/**
|
||||
* Screenshots organized by display type.
|
||||
* Key is the display type (e.g., 'APP_IPHONE_67'), value is array of file paths.
|
||||
* @example { "APP_IPHONE_67": ["./screenshots/home.png", "./screenshots/profile.png"] }
|
||||
*/
|
||||
export type AppleScreenshots = Partial<Record<AppleScreenshotDisplayType, string[]>>;
|
||||
|
||||
/**
|
||||
* Video preview configuration - either a simple path string or an object with options.
|
||||
* @example "./previews/demo.mp4"
|
||||
* @example { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" }
|
||||
*/
|
||||
export type ApplePreviewConfig =
|
||||
| string
|
||||
| {
|
||||
/** Video file path (relative to project root) */
|
||||
path: string;
|
||||
/** Optional preview frame time code (e.g., '00:05:00' for 5 seconds) */
|
||||
previewFrameTimeCode?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Video previews organized by display type.
|
||||
* Key is the display type (e.g., 'IPHONE_67'), value is the preview config.
|
||||
* @example { "IPHONE_67": "./previews/demo.mp4" }
|
||||
* @example { "IPHONE_67": { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" } }
|
||||
*/
|
||||
export type ApplePreviews = Partial<Record<ApplePreviewType, ApplePreviewConfig>>;
|
||||
|
||||
export interface AppleMetadata {
|
||||
version?: string;
|
||||
copyright?: string;
|
||||
@@ -48,7 +9,6 @@ export interface AppleMetadata {
|
||||
categories?: AppleCategory;
|
||||
release?: AppleRelease;
|
||||
advisory?: AppleAdvisory;
|
||||
/** @deprecated Use screenshots/previews in AppleInfo instead */
|
||||
preview?: Record<string, string[]>;
|
||||
review?: AppleReview;
|
||||
}
|
||||
@@ -80,10 +40,6 @@ export interface AppleInfo {
|
||||
privacyPolicyText?: string;
|
||||
privacyChoicesUrl?: string;
|
||||
supportUrl?: string;
|
||||
/** Screenshots for this locale, organized by display type */
|
||||
screenshots?: AppleScreenshots;
|
||||
/** Video previews for this locale, organized by display type */
|
||||
previews?: ApplePreviews;
|
||||
}
|
||||
|
||||
export interface AppleReview {
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function downloadMetadataAsync({
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleWriter();
|
||||
const tasks = createAppleTasks();
|
||||
const taskCtx = { app, projectDir };
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function uploadMetadataAsync({
|
||||
version: config.getVersion()?.versionString,
|
||||
});
|
||||
|
||||
const taskCtx = { app, projectDir };
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,7 @@ import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { resolveVcsClient } from '../../vcs';
|
||||
import {
|
||||
hasIgnoredIosProjectAsync,
|
||||
resolveWorkflowAsync,
|
||||
resolveWorkflowPerPlatformAsync,
|
||||
} from '../workflow';
|
||||
import { resolveWorkflowAsync, resolveWorkflowPerPlatformAsync } from '../workflow';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
@@ -90,21 +86,3 @@ describe(resolveWorkflowPerPlatformAsync, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(hasIgnoredIosProjectAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
test('returns true when ios project is ignored', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
|
||||
'./.easignore': 'ios/\n',
|
||||
},
|
||||
projectDir
|
||||
);
|
||||
|
||||
await expect(hasIgnoredIosProjectAsync(projectDir, vcsClient)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -137,21 +137,81 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('throws an error in non-interactive mode', async () => {
|
||||
it('auto-generates application id in non-interactive mode when suggestion is available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
name: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.jester.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
expo: { android: { package: 'com.jester.myapp' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses ios.bundleIdentifier as application id in non-interactive mode when available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "blah": {} }',
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp', ios: { bundleIdentifier: 'com.example.myapp' } } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.example.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '',
|
||||
exp: {} as any,
|
||||
projectId: '1234',
|
||||
exp: { slug: '' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
})
|
||||
|
||||
@@ -148,15 +148,8 @@ async function configureApplicationIdAsync({
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#package'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
@@ -166,25 +159,50 @@ async function configureApplicationIdAsync({
|
||||
|
||||
assert(paths.staticConfigPath, 'app.json must exist');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/android-package')
|
||||
)}`
|
||||
);
|
||||
let packageName: string;
|
||||
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
const { packageName } = await promptAsync({
|
||||
name: 'packageName',
|
||||
type: 'text',
|
||||
message: `What would you like your Android application id to be?`,
|
||||
initial: suggestedAndroidApplicationId,
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
if (nonInteractive) {
|
||||
let suggestedAndroidApplicationId: string | undefined;
|
||||
try {
|
||||
suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
} catch {
|
||||
// If we can't get a suggestion, we'll throw the non-interactive error below
|
||||
}
|
||||
if (!suggestedAndroidApplicationId) {
|
||||
throw new Error(
|
||||
`The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#package'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
packageName = suggestedAndroidApplicationId;
|
||||
Log.log(`Using automatically generated Android application id: ${chalk.bold(packageName)}`);
|
||||
} else {
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/android-package')
|
||||
)}`
|
||||
);
|
||||
|
||||
const result = await promptAsync({
|
||||
name: 'packageName',
|
||||
type: 'text',
|
||||
message: `What would you like your Android application id to be?`,
|
||||
initial: suggestedAndroidApplicationId,
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
packageName = result.packageName;
|
||||
}
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
|
||||
@@ -115,20 +115,81 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('throws an error in non-interactive mode', async () => {
|
||||
it('auto-generates bundle identifier in non-interactive mode when suggestion is available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
name: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "blah": {} }',
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.jester.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
expo: { ios: { bundleIdentifier: 'com.jester.myapp' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses android.package as bundle identifier in non-interactive mode when available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp', android: { package: 'com.example.myapp' } } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.example.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: {} as any,
|
||||
exp: { slug: '' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
})
|
||||
|
||||
@@ -7,23 +7,6 @@ import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../exe
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
// @expo/require-utils uses require() to load JS configs, which bypasses the
|
||||
// mocked fs. Override loadModuleSync to read from the mocked fs and evaluate.
|
||||
jest.mock('@expo/require-utils', () => {
|
||||
const actual = jest.requireActual('@expo/require-utils');
|
||||
return {
|
||||
...actual,
|
||||
loadModuleSync: (filename: string) => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync(filename, 'utf-8');
|
||||
const mod = { exports: {} as any };
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function('module', 'exports', content)(mod, mod.exports);
|
||||
return mod.exports;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
vol.reset();
|
||||
|
||||
@@ -137,15 +137,8 @@ async function configureBundleIdentifierAsync({
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
@@ -155,26 +148,51 @@ async function configureBundleIdentifierAsync({
|
||||
|
||||
assert(paths.staticConfigPath, 'app.json must exist');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/bundle-identifier')
|
||||
)}`
|
||||
);
|
||||
let bundleIdentifier: string;
|
||||
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
if (nonInteractive) {
|
||||
let suggestedBundleIdentifier: string | undefined;
|
||||
try {
|
||||
suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
} catch {
|
||||
// If we can't get a suggestion, we'll throw the non-interactive error below
|
||||
}
|
||||
if (!suggestedBundleIdentifier) {
|
||||
throw new Error(
|
||||
`The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
bundleIdentifier = suggestedBundleIdentifier;
|
||||
Log.log(`Using automatically generated iOS bundle identifier: ${chalk.bold(bundleIdentifier)}`);
|
||||
} else {
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/bundle-identifier')
|
||||
)}`
|
||||
);
|
||||
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
type: 'text',
|
||||
message: `What would you like your iOS bundle identifier to be?`,
|
||||
initial: suggestedBundleIdentifier,
|
||||
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
|
||||
});
|
||||
const result = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
type: 'text',
|
||||
message: `What would you like your iOS bundle identifier to be?`,
|
||||
initial: suggestedBundleIdentifier,
|
||||
validate: value =>
|
||||
isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE,
|
||||
});
|
||||
bundleIdentifier = result.bundleIdentifier;
|
||||
}
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function hasIgnoredIosProjectAsync(
|
||||
try {
|
||||
const pbxProjectPath = IOSConfig.Paths.getPBXProjectPath(projectDir);
|
||||
return await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, pbxProjectPath));
|
||||
} catch {
|
||||
} finally {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.23.5",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.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,11 +26,11 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/build-tools": "18.4.0",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"bunyan": "^1.8.15",
|
||||
"chalk": "4.1.2",
|
||||
"env-paths": "^2.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/logger",
|
||||
"version": "18.5.0",
|
||||
"version": "18.0.1",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -27,8 +27,8 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"arg": "^5.0.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/template-file",
|
||||
"version": "18.5.0",
|
||||
"version": "18.0.1",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/turtle-spawn",
|
||||
"version": "18.5.0",
|
||||
"version": "18.0.1",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.5.0",
|
||||
"version": "18.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,18 +19,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.5.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "18.5.0",
|
||||
"@expo/eas-build-job": "18.5.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/build-tools": "18.4.0",
|
||||
"@expo/config-plugins": "8.0.8",
|
||||
"@expo/config-types": "52.0.1",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.5.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/steps": "18.4.0",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@hapi/boom": "10.0.1",
|
||||
"@sentry/node": "7.77.0",
|
||||
"check-disk-space": "3.4.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PRECOMPILED_MODULES_PATH } from '@expo/build-tools';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import config from '../config';
|
||||
@@ -34,7 +35,7 @@ describe(getBuildEnv.name, () => {
|
||||
expect(env.EAS_CLI_SENTRY_DSN).toBe(config.sentry.dsn);
|
||||
});
|
||||
|
||||
it('passes through EXPO_USE_PRECOMPILED_MODULES for flagged iOS jobs', () => {
|
||||
it('enables precompiled modules env vars for flagged iOS jobs', () => {
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
@@ -55,6 +56,6 @@ describe(getBuildEnv.name, () => {
|
||||
});
|
||||
|
||||
expect(env.EXPO_USE_PRECOMPILED_MODULES).toBe('1');
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBeUndefined();
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBe(PRECOMPILED_MODULES_PATH);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Artifacts, BuildContext, Builders, runGenericJobAsync } from '@expo/build-tools';
|
||||
import {
|
||||
Artifacts,
|
||||
BuildContext,
|
||||
Builders,
|
||||
maybeStartPreparingPrecompiledModules,
|
||||
runGenericJobAsync,
|
||||
} from '@expo/build-tools';
|
||||
import {
|
||||
Android,
|
||||
BuildJob,
|
||||
@@ -41,6 +47,7 @@ export async function build({
|
||||
{ job: omit(ctx.job, 'secrets', 'projectArchive') },
|
||||
'Builder is ready, starting build'
|
||||
);
|
||||
maybeStartPreparingPrecompiledModules(ctx, config);
|
||||
},
|
||||
{ doNotMarkStart: true }
|
||||
);
|
||||
|
||||
@@ -98,6 +98,10 @@ export default {
|
||||
transform: createBase64EnvTransformer('cocoapodsCacheUrl'),
|
||||
defaultValue: null,
|
||||
}),
|
||||
precompiledModulesUrls: [
|
||||
'https://storage.googleapis.com/turtle-v2/precompiled-modules/db65b4afac835ff71269ce53937fb20627b133c0/xcframeworks-Debug.zip',
|
||||
'https://storage.googleapis.com/turtle-v2/precompiled-modules/db65b4afac835ff71269ce53937fb20627b133c0/xcframeworks-Release.zip',
|
||||
],
|
||||
runMetricsServer: env<boolean | null>('WORKER_RUNTIME_CONFIG_BASE64', {
|
||||
transform: createBase64EnvTransformer('runMetricsServer'),
|
||||
defaultValue: null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PRECOMPILED_MODULES_PATH, shouldUsePrecompiledDependencies } from '@expo/build-tools';
|
||||
import { Env, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { spawnSync } from 'child_process';
|
||||
import micromatch from 'micromatch';
|
||||
@@ -53,8 +54,9 @@ export function getBuildEnv({
|
||||
if (runnerPlatform === Platform.IOS) {
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', config.cocoapodsCacheUrl);
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
if (job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES === '1') {
|
||||
if (shouldUsePrecompiledDependencies(job.builderEnvironment?.env ?? {})) {
|
||||
setEnv(env, 'EXPO_USE_PRECOMPILED_MODULES', '1');
|
||||
setEnv(env, 'EXPO_PRECOMPILED_MODULES_PATH', PRECOMPILED_MODULES_PATH);
|
||||
}
|
||||
|
||||
if (job.builderEnvironment?.env?.EAS_USE_CACHE === '1') {
|
||||
|
||||
+4
-13
@@ -82,7 +82,6 @@ export async function logProjectDependenciesAsync(
|
||||
});
|
||||
} catch (error: any) {
|
||||
sentry.handleError('Failed to report project dependencies metrics', error, {
|
||||
level: 'warning',
|
||||
tags: {
|
||||
errorCode: 'FAILED_TO_REPORT_PROJECT_DEPENDENCIES_EVENT',
|
||||
},
|
||||
@@ -134,15 +133,10 @@ async function resolveNewArchEnabled(
|
||||
}
|
||||
|
||||
if (
|
||||
(appConfig?.[job.platform] as unknown as Record<string, unknown>)?.newArchEnabled !==
|
||||
undefined ||
|
||||
(appConfig as unknown as Record<string, unknown>).newArchEnabled !== undefined
|
||||
appConfig?.[job.platform]?.newArchEnabled !== undefined ||
|
||||
appConfig.newArchEnabled !== undefined
|
||||
) {
|
||||
return (
|
||||
((appConfig?.[job.platform] as unknown as Record<string, unknown>)
|
||||
?.newArchEnabled as boolean) ??
|
||||
((appConfig as unknown as Record<string, unknown>).newArchEnabled as boolean)
|
||||
);
|
||||
return appConfig?.[job.platform]?.newArchEnabled ?? appConfig.newArchEnabled;
|
||||
}
|
||||
|
||||
if (job.type === Workflow.GENERIC) {
|
||||
@@ -226,10 +220,7 @@ async function getPackageManagerVersion(
|
||||
}
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], {
|
||||
cwd,
|
||||
env,
|
||||
});
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], { cwd, env });
|
||||
return stdout.toString().trim();
|
||||
} catch {
|
||||
// Some package managers can fail version checks in project dirs with mismatched packageManager fields.
|
||||
|
||||
@@ -844,17 +844,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/code-frame@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
js-tokens: "npm:^4.0.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.22.13":
|
||||
version: 7.22.13
|
||||
resolution: "@babel/code-frame@npm:7.22.13"
|
||||
@@ -922,13 +911,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/compat-data@npm:^7.28.6":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/compat-data@npm:7.29.0"
|
||||
checksum: 10c0/08f348554989d23aa801bf1405aa34b15e841c0d52d79da7e524285c77a5f9d298e70e11d91cc578d8e2c9542efc586d50c5f5cf8e1915b254a9dcf786913a94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:7.24.7":
|
||||
version: 7.24.7
|
||||
resolution: "@babel/core@npm:7.24.7"
|
||||
@@ -1044,29 +1026,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.25.2":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/core@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.29.0"
|
||||
"@babel/generator": "npm:^7.29.0"
|
||||
"@babel/helper-compilation-targets": "npm:^7.28.6"
|
||||
"@babel/helper-module-transforms": "npm:^7.28.6"
|
||||
"@babel/helpers": "npm:^7.28.6"
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/traverse": "npm:^7.29.0"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
"@jridgewell/remapping": "npm:^2.3.5"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 10c0/5127d2e8e842ae409e11bcbb5c2dff9874abf5415e8026925af7308e903f4f43397341467a130490d1a39884f461bc2b67f3063bce0be44340db89687fd852aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.16.5":
|
||||
version: 7.16.5
|
||||
resolution: "@babel/generator@npm:7.16.5"
|
||||
@@ -1148,19 +1107,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.29.0":
|
||||
version: 7.29.1
|
||||
resolution: "@babel/generator@npm:7.29.1"
|
||||
dependencies:
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.12"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.28"
|
||||
jsesc: "npm:^3.0.2"
|
||||
checksum: 10c0/349086e6876258ef3fb2823030fee0f6c0eb9c3ebe35fc572e16997f8c030d765f636ddc6299edae63e760ea6658f8ee9a2edfa6d6b24c9a80c917916b973551
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.7.2":
|
||||
version: 7.15.0
|
||||
resolution: "@babel/generator@npm:7.15.0"
|
||||
@@ -1270,19 +1216,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-compilation-targets@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-compilation-targets@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/compat-data": "npm:^7.28.6"
|
||||
"@babel/helper-validator-option": "npm:^7.27.1"
|
||||
browserslist: "npm:^4.24.0"
|
||||
lru-cache: "npm:^5.1.1"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 10c0/3fcdf3b1b857a1578e99d20508859dbd3f22f3c87b8a0f3dc540627b4be539bae7f6e61e49d931542fe5b557545347272bbdacd7f58a5c77025a18b745593a50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-create-class-features-plugin@npm:^7.12.1":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/helper-create-class-features-plugin@npm:7.12.1"
|
||||
@@ -1435,16 +1368,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-imports@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-module-imports@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/traverse": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.28.6"
|
||||
checksum: 10c0/b49d8d8f204d9dbfd5ac70c54e533e5269afb3cea966a9d976722b13e9922cc773a653405f53c89acb247d5aebdae4681d631a3ae3df77ec046b58da76eda2ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.12.1":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/helper-module-transforms@npm:7.12.1"
|
||||
@@ -1538,19 +1461,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-module-transforms@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/helper-module-imports": "npm:^7.28.6"
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
"@babel/traverse": "npm:^7.28.6"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: 10c0/6f03e14fc30b287ce0b839474b5f271e72837d0cafe6b172d759184d998fbee3903a035e81e07c2c596449e504f453463d58baa65b6f40a37ded5bec74620b2b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-optimise-call-expression@npm:^7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/helper-optimise-call-expression@npm:7.10.4"
|
||||
@@ -1588,13 +1498,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.28.6"
|
||||
checksum: 10c0/3f5f8acc152fdbb69a84b8624145ff4f9b9f6e776cb989f9f968f8606eb7185c5c3cfcf3ba08534e37e1e0e1c118ac67080610333f56baa4f7376c99b5f1143d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-replace-supers@npm:^7.12.1":
|
||||
version: 7.12.5
|
||||
resolution: "@babel/helper-replace-supers@npm:7.12.5"
|
||||
@@ -1857,16 +1760,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.28.6":
|
||||
version: 7.29.2
|
||||
resolution: "@babel/helpers@npm:7.29.2"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
checksum: 10c0/dab0e65b9318b2502a62c58bc0913572318595eec0482c31f0ad416b72636e6698a1d7c57cd2791d4528eb8c548bca88d338dc4d2a55a108dc1f6702f9bc5512
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/highlight@npm:^7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/highlight@npm:7.10.4"
|
||||
@@ -2029,17 +1922,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0":
|
||||
version: 7.29.2
|
||||
resolution: "@babel/parser@npm:7.29.2"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 10c0/e5a4e69e3ac7acdde995f37cf299a68458cfe7009dff66bd0962fd04920bef287201169006af365af479c08ff216bfefbb595e331f87f6ae7283858aebbc3317
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-properties@npm:^7.0.0":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/plugin-proposal-class-properties@npm:7.12.1"
|
||||
@@ -2396,18 +2278,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs@npm:^7.24.8":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms": "npm:^7.28.6"
|
||||
"@babel/helper-plugin-utils": "npm:^7.28.6"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/7c45992797c6150644c8552feff4a016ba7bd6d59ff2b039ed969a9c5b20a6804cd9d21db5045fc8cca8ca7f08262497e354e93f8f2be6a1cdf3fbfa8c31a9b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-object-super@npm:^7.0.0":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/plugin-transform-object-super@npm:7.12.1"
|
||||
@@ -2587,17 +2457,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/template@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.28.6"
|
||||
"@babel/parser": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.28.6"
|
||||
checksum: 10c0/66d87225ed0bc77f888181ae2d97845021838c619944877f7c4398c6748bcf611f216dfd6be74d39016af502bca876e6ce6873db3c49e4ac354c56d34d57e9f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.12.1, @babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.5, @babel/traverse@npm:^7.16.7, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.17.10, @babel/traverse@npm:^7.17.3":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/traverse@npm:7.23.2"
|
||||
@@ -2649,21 +2508,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/traverse@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.29.0"
|
||||
"@babel/generator": "npm:^7.29.0"
|
||||
"@babel/helper-globals": "npm:^7.28.0"
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
debug: "npm:^4.3.1"
|
||||
checksum: 10c0/f63ef6e58d02a9fbf3c0e2e5f1c877da3e0bc57f91a19d2223d53e356a76859cbaf51171c9211c71816d94a0e69efa2732fd27ffc0e1bbc84b636e60932333eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.11.0, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3":
|
||||
version: 7.12.6
|
||||
resolution: "@babel/types@npm:7.12.6"
|
||||
@@ -2779,7 +2623,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.8.3":
|
||||
"@babel/types@npm:^7.8.3":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/types@npm:7.29.0"
|
||||
dependencies:
|
||||
@@ -2847,33 +2691,33 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/apple-utils@npm:2.1.16":
|
||||
version: 2.1.16
|
||||
resolution: "@expo/apple-utils@npm:2.1.16"
|
||||
"@expo/apple-utils@npm:2.1.15":
|
||||
version: 2.1.15
|
||||
resolution: "@expo/apple-utils@npm:2.1.15"
|
||||
bin:
|
||||
apple-utils: bin.js
|
||||
checksum: 10c0/9d3915f3f25c32e1f1ab323676cabccc2998046be6f30deebc68a0706836bd50bc5145662fa13441e0283f82c56e50f55002d50dcc43aa78e9fa7327d1676a54
|
||||
checksum: 10c0/956abdb77d1d3bcb09d42b70745705305d23b9aa4ed50e3da795e93e50dc3ada126cc16f73a55b49538f0d793ccdc455bad0caf7aaadbadd5ee7a3d0f3c69465
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:18.5.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:18.4.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/config": "npm:10.0.6"
|
||||
"@expo/config-plugins": "npm:9.0.12"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/env": "npm:^0.4.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/plist": "npm:^0.2.0"
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/template-file": "npm:18.0.1"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@expo/xcpretty": "npm:^4.3.1"
|
||||
"@google-cloud/storage": "npm:^7.11.2"
|
||||
"@types/fs-extra": "npm:^11.0.4"
|
||||
@@ -2948,24 +2792,48 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:55.0.7, @expo/config-plugins@npm:~55.0.7":
|
||||
version: 55.0.7
|
||||
resolution: "@expo/config-plugins@npm:55.0.7"
|
||||
"@expo/config-plugins@npm:8.0.8":
|
||||
version: 8.0.8
|
||||
resolution: "@expo/config-plugins@npm:8.0.8"
|
||||
dependencies:
|
||||
"@expo/config-types": "npm:^55.0.5"
|
||||
"@expo/json-file": "npm:~10.0.12"
|
||||
"@expo/plist": "npm:^0.5.2"
|
||||
"@expo/config-types": "npm:^51.0.0-unreleased"
|
||||
"@expo/json-file": "npm:~8.3.0"
|
||||
"@expo/plist": "npm:^0.1.0"
|
||||
"@expo/sdk-runtime-versions": "npm:^1.0.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
debug: "npm:^4.3.5"
|
||||
getenv: "npm:^2.0.0"
|
||||
glob: "npm:^13.0.0"
|
||||
debug: "npm:^4.3.1"
|
||||
find-up: "npm:~5.0.0"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:7.1.6"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
semver: "npm:^7.5.4"
|
||||
slash: "npm:^3.0.0"
|
||||
slugify: "npm:^1.6.6"
|
||||
xcode: "npm:^3.0.1"
|
||||
xml2js: "npm:0.6.0"
|
||||
checksum: 10c0/46cea118d9a780ac367862ef136aafa65063a3bc9a9864ece2a18d4b9e66e64d2bb19f0c0972e07f1106a58203f65671208bebcf5e207953f54217210807746c
|
||||
checksum: 10c0/4d403ce9df0cd694cd30f1a914c4e4529c529e9b5f572ddc3cdf88f80d4c47a8ba8336da78ae3f9c67966a7fa1e02d87ea2dff95102a0a337004d118ebc32d63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:9.0.12":
|
||||
version: 9.0.12
|
||||
resolution: "@expo/config-plugins@npm:9.0.12"
|
||||
dependencies:
|
||||
"@expo/config-types": "npm:^52.0.0"
|
||||
"@expo/json-file": "npm:~9.0.0"
|
||||
"@expo/plist": "npm:^0.2.0"
|
||||
"@expo/sdk-runtime-versions": "npm:^1.0.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
debug: "npm:^4.3.5"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:^10.4.2"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
semver: "npm:^7.5.4"
|
||||
slash: "npm:^3.0.0"
|
||||
slugify: "npm:^1.6.6"
|
||||
xcode: "npm:^3.0.1"
|
||||
xml2js: "npm:0.6.0"
|
||||
checksum: 10c0/a27f085caf3d9ce29ca8b532425dd409054d8ef449b5d5338db2a2b1ead8051073e4f155691f6c2dc40b46816229fffbd09ef51c84eb87aa95eefaea9c810d40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2991,7 +2859,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:~9.0.0, @expo/config-plugins@npm:~9.0.17":
|
||||
"@expo/config-plugins@npm:~9.0.0, @expo/config-plugins@npm:~9.0.10, @expo/config-plugins@npm:~9.0.17":
|
||||
version: 9.0.17
|
||||
resolution: "@expo/config-plugins@npm:9.0.17"
|
||||
dependencies:
|
||||
@@ -3013,10 +2881,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-types@npm:55.0.5, @expo/config-types@npm:^55.0.5":
|
||||
version: 55.0.5
|
||||
resolution: "@expo/config-types@npm:55.0.5"
|
||||
checksum: 10c0/24ce0481cc465ddd3b53cfdde099ef4e899b1f8fff224a0f249b88c93e6c98930e99a55f3929eb53d08138b1b66102ece7b76e16f4e5fadcdf5bbac26c9c3d7e
|
||||
"@expo/config-types@npm:52.0.1":
|
||||
version: 52.0.1
|
||||
resolution: "@expo/config-types@npm:52.0.1"
|
||||
checksum: 10c0/f06287eb20d599fbc422bf58dc97a803d8f9242699ce43188b90bfd92d54da27f83ca1225bf1ec61a5c30e74c57e47e7bfba2ef3cada38983622657658fafe4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-types@npm:^51.0.0-unreleased":
|
||||
version: 51.0.3
|
||||
resolution: "@expo/config-types@npm:51.0.3"
|
||||
checksum: 10c0/bd87a729da985b0097ab29367c0473a2bced5ff211d416f342729e1b2631a7c00d62878e05cdee49ece7e3b65d3296957917f24380d57e2faef2cf220194fdec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3034,6 +2909,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:10.0.6":
|
||||
version: 10.0.6
|
||||
resolution: "@expo/config@npm:10.0.6"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:~7.10.4"
|
||||
"@expo/config-plugins": "npm:~9.0.10"
|
||||
"@expo/config-types": "npm:^52.0.0"
|
||||
"@expo/json-file": "npm:^9.0.0"
|
||||
deepmerge: "npm:^4.3.1"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:^10.4.2"
|
||||
require-from-string: "npm:^2.0.2"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
resolve-workspace-root: "npm:^2.0.0"
|
||||
semver: "npm:^7.6.0"
|
||||
slugify: "npm:^1.3.4"
|
||||
sucrase: "npm:3.35.0"
|
||||
checksum: 10c0/1fdf47ea0e268b475e30e02ebdc1f6739c0b426a4c6c695a7b4ee680109258b4ccfbd3010d3bd031a6efcd7a861670be84d10cd0019d0f5d14dc669a5039b860
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:11.0.10":
|
||||
version: 11.0.10
|
||||
resolution: "@expo/config@npm:11.0.10"
|
||||
@@ -3055,25 +2951,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:55.0.10":
|
||||
version: 55.0.10
|
||||
resolution: "@expo/config@npm:55.0.10"
|
||||
dependencies:
|
||||
"@expo/config-plugins": "npm:~55.0.7"
|
||||
"@expo/config-types": "npm:^55.0.5"
|
||||
"@expo/json-file": "npm:^10.0.12"
|
||||
"@expo/require-utils": "npm:^55.0.3"
|
||||
deepmerge: "npm:^4.3.1"
|
||||
getenv: "npm:^2.0.0"
|
||||
glob: "npm:^13.0.0"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
resolve-workspace-root: "npm:^2.0.0"
|
||||
semver: "npm:^7.6.0"
|
||||
slugify: "npm:^1.3.4"
|
||||
checksum: 10c0/a2e0a100c8d472e56e9ad649a8852eddca76ab64503475c803b4fc372df08e303169243b6a782fe150c54212ba02f47c40c4f0e365c13e77e70f5d777aa8d704
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:~10.0.4":
|
||||
version: 10.0.11
|
||||
resolution: "@expo/config@npm:10.0.11"
|
||||
@@ -3095,7 +2972,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/downloader@npm:18.5.0, @expo/downloader@workspace:packages/downloader":
|
||||
"@expo/downloader@npm:18.0.1, @expo/downloader@workspace:packages/downloader":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/downloader@workspace:packages/downloader"
|
||||
dependencies:
|
||||
@@ -3111,11 +2988,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-build-job@npm:18.5.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
"@expo/eas-build-job@npm:18.4.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-build-job@workspace:packages/eas-build-job"
|
||||
dependencies:
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
jest: "npm:^29.7.0"
|
||||
@@ -3128,12 +3005,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-json@npm:18.5.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
"@expo/eas-json@npm:18.4.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-json@workspace:packages/eas-json"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.23.5"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@tsconfig/node20": "npm:20.1.8"
|
||||
"@types/babel__code-frame": "npm:7.0.3"
|
||||
"@types/fs-extra": "npm:11.0.4"
|
||||
@@ -3197,7 +3074,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:8.3.3":
|
||||
"@expo/json-file@npm:8.3.3, @expo/json-file@npm:~8.3.0":
|
||||
version: 8.3.3
|
||||
resolution: "@expo/json-file@npm:8.3.3"
|
||||
dependencies:
|
||||
@@ -3208,16 +3085,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:^10.0.12, @expo/json-file@npm:~10.0.12":
|
||||
version: 10.0.12
|
||||
resolution: "@expo/json-file@npm:10.0.12"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.20.0"
|
||||
json5: "npm:^2.2.3"
|
||||
checksum: 10c0/52131a6426e96208ff1b295d580fc70eebb8e292b29fde1db016b2f21a0942a7521feec96b3f58efe5b32dcc1642d569b4211d651146fcdb9bf7e5f08b635878
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:^10.0.8":
|
||||
version: 10.0.8
|
||||
resolution: "@expo/json-file@npm:10.0.8"
|
||||
@@ -3259,7 +3126,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:~9.0.2":
|
||||
"@expo/json-file@npm:~9.0.0, @expo/json-file@npm:~9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "@expo/json-file@npm:9.0.2"
|
||||
dependencies:
|
||||
@@ -3270,7 +3137,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/logger@npm:18.5.0, @expo/logger@workspace:packages/logger":
|
||||
"@expo/logger@npm:18.0.1, @expo/logger@workspace:packages/logger":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/logger@workspace:packages/logger"
|
||||
dependencies:
|
||||
@@ -3338,6 +3205,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.1.0":
|
||||
version: 0.1.3
|
||||
resolution: "@expo/plist@npm:0.1.3"
|
||||
dependencies:
|
||||
"@xmldom/xmldom": "npm:~0.7.7"
|
||||
base64-js: "npm:^1.2.3"
|
||||
xmlbuilder: "npm:^14.0.0"
|
||||
checksum: 10c0/134315260a7828bc1ce4563e2af67499b498feae46c39c5c2cab9d72082402a42d3b7575f13e269022bcf3c28668948ea960dd4943bd38f52f9c01154317aac5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.2.0, @expo/plist@npm:^0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "@expo/plist@npm:0.2.2"
|
||||
@@ -3360,17 +3238,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "@expo/plist@npm:0.5.2"
|
||||
dependencies:
|
||||
"@xmldom/xmldom": "npm:^0.8.8"
|
||||
base64-js: "npm:^1.5.1"
|
||||
xmlbuilder: "npm:^15.1.1"
|
||||
checksum: 10c0/19adae2a365ac1a12db93682fb310ff8be03c711f9173bebe5841cbe60cdfb749247bc1a95fa0977b5bac3aa6a078a0fceeafe4ff6c66d1ed67cce496679e310
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plugin-help@npm:5.1.23":
|
||||
version: 5.1.23
|
||||
resolution: "@expo/plugin-help@npm:5.1.23"
|
||||
@@ -3427,22 +3294,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/require-utils@npm:^55.0.3":
|
||||
version: 55.0.3
|
||||
resolution: "@expo/require-utils@npm:55.0.3"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.20.0"
|
||||
"@babel/core": "npm:^7.25.2"
|
||||
"@babel/plugin-transform-modules-commonjs": "npm:^7.24.8"
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0 || ^5.0.0-0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/562a2dc71f983fba2215295bbcc376d6911217c3a98b96484331112ff98c7a2e979fb1904ac293008ac557114c6658c1deb9b2f441bd246764b507103d2560cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/results@npm:1.0.0, @expo/results@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@expo/results@npm:1.0.0"
|
||||
@@ -3481,12 +3332,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/steps@npm:18.5.0, @expo/steps@workspace:packages/steps":
|
||||
"@expo/steps@npm:18.4.0, @expo/steps@workspace:packages/steps":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/steps@workspace:packages/steps"
|
||||
dependencies:
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
@@ -3511,7 +3362,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/template-file@npm:18.5.0, @expo/template-file@workspace:packages/template-file":
|
||||
"@expo/template-file@npm:18.0.1, @expo/template-file@workspace:packages/template-file":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/template-file@workspace:packages/template-file"
|
||||
dependencies:
|
||||
@@ -3534,11 +3385,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/turtle-spawn@npm:18.5.0, @expo/turtle-spawn@workspace:packages/turtle-spawn":
|
||||
"@expo/turtle-spawn@npm:18.0.1, @expo/turtle-spawn@workspace:packages/turtle-spawn":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/turtle-spawn@workspace:packages/turtle-spawn"
|
||||
dependencies:
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -3554,18 +3405,18 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.5.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/build-tools": "npm:18.4.0"
|
||||
"@expo/config-plugins": "npm:8.0.8"
|
||||
"@expo/config-types": "npm:52.0.1"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/template-file": "npm:18.0.1"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@hapi/boom": "npm:10.0.1"
|
||||
"@sentry/node": "npm:7.77.0"
|
||||
"@types/babel__core": "npm:7.20.5"
|
||||
@@ -5202,7 +5053,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5":
|
||||
"@jridgewell/gen-mapping@npm:^0.3.12":
|
||||
version: 0.3.13
|
||||
resolution: "@jridgewell/gen-mapping@npm:0.3.13"
|
||||
dependencies:
|
||||
@@ -5223,16 +5074,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/remapping@npm:^2.3.5":
|
||||
version: 2.3.5
|
||||
resolution: "@jridgewell/remapping@npm:2.3.5"
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.5"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.24"
|
||||
checksum: 10c0/3de494219ffeb2c5c38711d0d7bb128097edf91893090a2dbc8ee0b55d092bb7347b1fd0f478486c5eab010e855c73927b1666f2107516d472d24a73017d1194
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/resolve-uri@npm:^3.0.3":
|
||||
version: 3.0.7
|
||||
resolution: "@jridgewell/resolve-uri@npm:3.0.7"
|
||||
@@ -10331,7 +10172,7 @@ __metadata:
|
||||
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
|
||||
dependencies:
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -10944,11 +10785,11 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:18.5.0"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/build-tools": "npm:18.4.0"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.0.1"
|
||||
"@types/bunyan": "npm:^1.8.11"
|
||||
"@types/fs-extra": "npm:^11.0.4"
|
||||
"@types/hapi__joi": "npm:^17.1.14"
|
||||
@@ -10998,15 +10839,15 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli@workspace:packages/eas-cli"
|
||||
dependencies:
|
||||
"@expo/apple-utils": "npm:2.1.16"
|
||||
"@expo/apple-utils": "npm:2.1.15"
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/eas-build-job": "npm:18.5.0"
|
||||
"@expo/eas-json": "npm:18.5.0"
|
||||
"@expo/config": "npm:10.0.6"
|
||||
"@expo/config-plugins": "npm:9.0.12"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/eas-json": "npm:18.4.0"
|
||||
"@expo/env": "npm:^1.0.0"
|
||||
"@expo/json-file": "npm:8.3.3"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
"@expo/multipart-body-parser": "npm:2.0.0"
|
||||
"@expo/osascript": "npm:2.1.4"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
@@ -11018,7 +10859,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.5.0"
|
||||
"@expo/steps": "npm:18.4.0"
|
||||
"@expo/timeago.js": "npm:1.0.0"
|
||||
"@graphql-codegen/cli": "npm:5.0.0"
|
||||
"@graphql-codegen/introspection": "npm:4.0.0"
|
||||
@@ -11960,7 +11801,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-up@npm:^5.0.0":
|
||||
"find-up@npm:^5.0.0, find-up@npm:~5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "find-up@npm:5.0.0"
|
||||
dependencies:
|
||||
@@ -12432,13 +12273,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"getenv@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "getenv@npm:2.0.0"
|
||||
checksum: 10c0/397ff641dd70cd78e414430258651e9a2228d3c5553a8cf15ae7840f75d3f10dfcb83f668f84829e84ea665b0fce2f08a9eddda3c9dcd7faa2d3da1c182c1854
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"git-hooks-list@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "git-hooks-list@npm:3.2.0"
|
||||
@@ -12543,6 +12377,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:7.1.6":
|
||||
version: 7.1.6
|
||||
resolution: "glob@npm:7.1.6"
|
||||
dependencies:
|
||||
fs.realpath: "npm:^1.0.0"
|
||||
inflight: "npm:^1.0.4"
|
||||
inherits: "npm:2"
|
||||
minimatch: "npm:^3.0.4"
|
||||
once: "npm:^1.3.0"
|
||||
path-is-absolute: "npm:^1.0.0"
|
||||
checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2":
|
||||
version: 10.3.10
|
||||
resolution: "glob@npm:10.3.10"
|
||||
|
||||
Reference in new issue
Block a user