Compare commits

..
Author SHA1 Message Date
Will Schurman 9147ae631c [rfc][eas-cli] Unify and globalize command non-interactive and json flags 2022-09-14 09:42:45 -07:00
Dominik Sokal 9c09cb9034 Update CHANGELOG.md 2022-09-12 09:28:21 +02:00
Dominik Sokal e52d14586f [ENG-6129] support json5 in eas.json (#1350)
* support json5 in eas.json

* address PR feedback

* address more PR feedback

* fix test
2022-09-12 09:27:01 +02:00
Szymon Dziedzic 0f7646868f [eas-cli][ENG-6229] add -e as a shortcut for --profile flag (#1342)
* [eas-cli] add `-e` flag as a shortcut for `--profile` flag

* revert readme changes

* add description for profile flag on config command

* update CHANGELOG.md
2022-09-07 19:36:23 +02:00
Justin McBride 6fe4511280 Small updates to the language of the Device Create command (#1346) 2022-09-07 09:28:53 +02:00
Expo CI efc1551d62 update CHANGELOG.md 2022-09-05 12:11:53 +00:00
39 changed files with 918 additions and 469 deletions

No files matched your search

+11
View File
@@ -8,6 +8,17 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `-e` as a shortcut for `--profile` flag. ([#1342](https://github.com/expo/eas-cli/pull/1342) by [@szdziedzic](https://github.com/szdziedzic))
- Support JSON5 in **eas.json**. ([#1350](https://github.com/expo/eas-cli/pull/1350) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
### 🧹 Chores
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
### 🎉 New features
- Add tvOS credentials compatibility for Adhoc and App store builds. ([#1325](https://github.com/expo/eas-cli/pull/1325) by [@quinlanj](https://github.com/quinlanj))
- Add `eas open` command for opening project page in web browser. ([#1337](https://github.com/expo/eas-cli/pull/1337) by [@dsokal](https://github.com/dsokal))
- Add support for buildArtifactPaths. Rename artifactPath to applicationArchivePath. ([#1321](https://github.com/expo/eas-cli/pull/1321) by [@dsokal](https://github.com/dsokal))
+3 -3
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonReader } from '@expo/eas-json';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -24,7 +24,7 @@ interface ConfigureParams {
export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams
): Promise<boolean> {
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) {
return false;
}
@@ -89,7 +89,7 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
};
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
@@ -3,7 +3,8 @@ import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonReader,
EasJsonAccessor,
EasJsonUtils,
SubmitProfile,
} from '@expo/eas-json';
import chalk from 'chalk';
@@ -94,13 +95,14 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonReader = new EasJsonReader(projectDir);
const easJsonCliConfig: EasJson['cli'] = (await easJsonReader.getCliConfigAsync()) ?? {};
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
});
@@ -150,7 +152,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.submitProfile,
type: 'submit',
@@ -1,8 +1,10 @@
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import { Command } from '@oclif/core';
import { Command, Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
// eslint-disable-next-line no-restricted-imports
import { Options, Ora } from 'ora';
import path from 'path';
import semver from 'semver';
@@ -12,15 +14,48 @@ import {
initAsync as initAnalyticsAsync,
logEvent,
} from '../analytics/rudderstackClient';
import { learnMore } from '../log';
import Log, { learnMore } from '../log';
import { interactiveOra, nonInteractiveOra } from '../ora';
import { findProjectRootAsync } from '../project/projectUtils';
import { InteractivePrompts, NonInteractivePrompts, Prompts } from '../prompts';
import { getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git';
type CommandOptions =
| {
nonInteractive: true;
json: true;
}
| {
nonInteractive: boolean;
json: false;
};
export type CommandContext = Readonly<
CommandOptions & {
logger: Log;
prompts: Prompts;
ora: (options?: Options | string) => Ora;
}
>;
export default abstract class EasCommand extends Command {
static override globalFlags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
dependsOn: ['non-interactive'],
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
// disable built in oclif json stuff
enableJsonFlag = false;
/**
* When user data is unavailable locally, determines if the command will
* force the user to log in
@@ -28,10 +63,26 @@ export default abstract class EasCommand extends Command {
protected requiresAuthentication = true;
protected mustBeRunInsideProject = true;
protected abstract runAsync(): Promise<any>;
protected abstract runAsync({
nonInteractive,
json,
logger,
prompts: {
promptAsync,
confirmAsync,
selectAsync,
toggleConfirmAsync,
pressAnyKeyToContinueAsync,
},
ora,
}: CommandContext): Promise<{ jsonOutput: object }>;
// eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> {
const { flags } = await this.parse();
const json = flags.json ?? false;
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await initAnalyticsAsync();
if (this.mustBeRunInsideProject) {
@@ -41,8 +92,6 @@ export default abstract class EasCommand extends Command {
}
if (this.requiresAuthentication) {
const { flags } = await this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
} else {
await getUserAsync();
@@ -52,7 +101,18 @@ export default abstract class EasCommand extends Command {
// commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`,
});
return this.runAsync();
const jsonOutput = await this.runAsync({
nonInteractive,
json: json ?? false,
logger: new Log(json),
prompts: nonInteractive ? new InteractivePrompts() : new NonInteractivePrompts(),
ora: nonInteractive ? nonInteractiveOra : interactiveOra,
});
if (json) {
this.printJsonOnlyOutput(jsonOutput);
}
}
// eslint-disable-next-line async-protect/async-suffix
@@ -62,8 +122,8 @@ export default abstract class EasCommand extends Command {
}
private async applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonReader = new EasJsonReader(projectDir);
const config = await easJsonReader.getCliConfigAsync();
const easJsonAccessor = new EasJsonAccessor(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
@@ -119,4 +179,26 @@ export default abstract class EasCommand extends Command {
packageJson?.devDependencies?.['eas-cli'] !== undefined
);
}
private printJsonOnlyOutput(value: object): void {
try {
new Log(false).log(JSON.stringify(this.sanitizeValue(value), null, 2));
} catch {}
}
private sanitizeValue(value: any): unknown {
if (Array.isArray(value)) {
return value.map(val => this.sanitizeValue(val));
} else if (value && typeof value === 'object') {
const result: Record<string, any> = {};
Object.keys(value).forEach(key => {
if (key !== '__typename' && value[key] !== null) {
result[key] = this.sanitizeValue(value[key]);
}
});
return result;
} else {
return value;
}
}
}
@@ -1,12 +1,15 @@
import { Flags } from '@oclif/core';
export const getPaginatedQueryOptions = (
flags: Record<keyof typeof EasPaginatedQueryFlags, any>
): PaginatedQueryOptions => {
export const getPaginatedQueryOptions = (flags: {
json: boolean;
'non-interactive': boolean;
offset: number | undefined;
limit: number | undefined;
}): PaginatedQueryOptions => {
return {
json: flags.json ?? false,
json: flags.json,
offset: flags.offset ?? 0,
nonInteractive: flags['non-interactive'] ?? false,
nonInteractive: flags['non-interactive'],
...('limit' in flags && { limit: flags.limit }),
};
};
@@ -40,12 +43,6 @@ export const EasPaginatedQueryFlags = {
// eslint-disable-next-line async-protect/async-suffix
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, 100),
}),
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}),
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
}),
};
// options required to control a paginated query
+17 -29
View File
@@ -1,8 +1,7 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateBranchMutation,
@@ -11,15 +10,12 @@ import {
GetBranchInfoQuery,
GetBranchInfoQueryVariables,
} from '../../graphql/generated';
import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
async function getBranchInfoAsync({
appId,
@@ -86,21 +82,15 @@ export default class BranchDelete extends EasCommand {
description: 'Name of the branch to delete',
},
];
static override flags = {
json: Flags.boolean({
description: `return JSON with the edited branch's ID and name.`,
default: false,
}),
};
async runAsync(): Promise<void> {
protected async runAsync({
nonInteractive,
logger,
prompts: { promptAsync, toggleConfirmAsync },
}: CommandContext): Promise<{ jsonOutput: object }> {
let {
args: { name },
flags: { json: jsonFlag },
} = await this.parse(BranchDelete);
if (jsonFlag) {
enableJsonOutput();
}
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
@@ -109,7 +99,7 @@ export default class BranchDelete extends EasCommand {
if (!name) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
if (nonInteractive) {
throw new Error(validationMessage);
}
({ name } = await promptAsync({
@@ -126,16 +116,16 @@ export default class BranchDelete extends EasCommand {
throw new Error(`Could not find branch ${name} on ${fullName}`);
}
if (!jsonFlag) {
Log.addNewLineIfNone();
Log.warn(
if (!nonInteractive) {
logger.addNewLineIfNone();
logger.warn(
`You are about to permanently delete branch: "${name}" and all of the updates published on it.` +
`\nThis action is irreversible.`
);
Log.newLine();
logger.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.error(`Cancelled deletion of branch: "${name}".`);
logger.error(`Cancelled deletion of branch: "${name}".`);
process.exit(1);
}
}
@@ -144,12 +134,10 @@ export default class BranchDelete extends EasCommand {
branchId,
});
if (jsonFlag) {
printJsonOnlyOutput(deletionResult);
} else {
Log.withTick(
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
}
logger.withTick(
`Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
);
return { jsonOutput: deletionResult };
}
}
+17 -12
View File
@@ -1,9 +1,8 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import figures from 'figures';
import fs from 'fs-extra';
import path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
@@ -56,6 +55,7 @@ export default class Build extends EasCommand {
hidden: true,
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -222,8 +222,8 @@ async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
const reader = new EasJsonReader(projectDir);
const profileNames = await reader.getBuildProfileNamesAsync();
const easJsonAccessor = new EasJsonAccessor(projectDir);
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
[Platform.ANDROID, profileName],
[Platform.IOS, profileName],
@@ -232,7 +232,11 @@ async function handleDeprecatedEasJsonAsync(
const deprecatedProfiles: [Platform, string][] = [];
for (const [platform, profileName] of platformAndProfileNames) {
const buildProfile = await reader.getBuildProfileAsync(platform, profileName);
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
profileName
);
if (buildProfile.artifactPath) {
deprecatedProfiles.push([platform, profileName]);
}
@@ -275,15 +279,16 @@ async function handleDeprecatedEasJsonAsync(
Errors.exit(1);
}
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const easJson = await fs.readJSON(easJsonPath);
await easJsonAccessor.readRawJsonAsync();
for (const [platform, profileName] of deprecatedProfiles) {
easJson.build[profileName][platform].applicationArchivePath =
easJson.build[profileName][platform].artifactPath;
delete easJson.build[profileName][platform].artifactPath;
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.build[profileName][platform].applicationArchivePath =
easJsonRawObject.build[profileName][platform].artifactPath;
delete easJsonRawObject.build[profileName][platform].artifactPath;
return easJsonRawObject;
});
}
await easJsonAccessor.writeAsync();
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
Log.withTick('Updated eas.json');
}
@@ -35,6 +35,7 @@ export default class BuildInspect extends EasCommand {
required: true,
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -1,6 +1,6 @@
import { getRuntimeVersionNullable } from '@expo/config-plugins/build/utils/Updates';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -35,6 +35,7 @@ export default class BuildVersionSetView extends EasCommand {
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -47,9 +48,13 @@ export default class BuildVersionSetView extends EasCommand {
const projectDir = await findProjectRootAsync();
const platform = await selectPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const profile = await easJsonReader.getBuildProfileAsync(platform, flags.profile ?? undefined);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const profile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
flags.profile ?? undefined
);
const exp = getExpoConfig(projectDir, { env: profile.env });
const projectId = await getProjectIdAsync(exp);
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -49,6 +49,7 @@ export default class BuildVersionSyncView extends EasCommand {
options: ['android', 'ios', 'all'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
@@ -60,13 +61,13 @@ export default class BuildVersionSyncView extends EasCommand {
const projectDir = await findProjectRootAsync();
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const platforms = toPlatforms(requestedPlatform);
const buildProfiles = await getProfilesAsync({
type: 'build',
easJsonReader,
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
});
+10 -5
View File
@@ -1,6 +1,6 @@
import { getProjectConfigDescription } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -17,7 +17,12 @@ export default class Config extends EasCommand {
static override flags = {
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
profile: Flags.string(),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
};
protected override requiresAuthentication = false;
@@ -31,13 +36,13 @@ export default class Config extends EasCommand {
const projectDir = await findProjectRootAsync();
const reader = new EasJsonReader(projectDir);
const accessor = new EasJsonAccessor(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
'Select build profile',
(
await reader.getBuildProfileNamesAsync()
await EasJsonUtils.getBuildProfileNamesAsync(accessor)
).map(profileName => ({
title: profileName,
value: profileName,
@@ -56,7 +61,7 @@ export default class Config extends EasCommand {
},
]));
const profile = await reader.getBuildProfileAsync(platform, profileName);
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
Log.addNewLineIfNone();
+59 -35
View File
@@ -2,7 +2,7 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
import { Flags } from '@oclif/core';
import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand';
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import {
@@ -14,11 +14,8 @@ import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleT
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand {
static override description = 'remove a registered device from your account';
@@ -28,7 +25,7 @@ export default class DeviceDelete extends EasCommand {
udid: Flags.string({ multiple: true }),
};
async runAsync(): Promise<void> {
protected async runAsync(commandContext: CommandContext): Promise<{ jsonOutput: object }> {
let {
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
} = await this.parse(DeviceDelete);
@@ -38,46 +35,61 @@ export default class DeviceDelete extends EasCommand {
const accountName = await getProjectAccountNameAsync(exp);
if (!appleTeamIdentifier) {
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
if (commandContext.nonInteractive) {
throw new Error('Must supply `apple-team-id` in non-interactive mode');
}
appleTeamIdentifier = await this.askForAppleTeamAsync(commandContext, accountName);
}
assert(appleTeamIdentifier, 'No team identifier is specified');
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
const appleDevicesResult = await this.getDevicesForTeamAsync(
commandContext,
accountName,
appleTeamIdentifier
);
if (!appleDevicesResult) {
return;
return { jsonOutput: [] };
}
const { appleTeamName, appleDevices } = appleDevicesResult;
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
const chosenDevices = await this.chooseDevicesToDeleteAsync(
commandContext,
appleDevices,
udids
);
if (chosenDevices.length === 0) {
Log.newLine();
Log.warn('No devices were chosen to be removed.');
return;
commandContext.logger.newLine();
commandContext.logger.warn('No devices were chosen to be removed.');
return { jsonOutput: [] };
}
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
this.logChosenDevices(commandContext, chosenDevices, appleTeamName, appleTeamIdentifier);
const hasRemoved = await this.askAndRemoveFromExpoAsync(commandContext, chosenDevices);
if (!hasRemoved) {
return;
return { jsonOutput: [] };
}
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
await this.askAndDisableOnAppleAsync(commandContext, chosenDevices, appleTeamIdentifier);
return { jsonOutput: chosenDevices.map(it => it.id) };
}
async askAndDisableOnAppleAsync(
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamIdentifier: string
): Promise<void> {
Log.newLine();
const deleteOnApple = await toggleConfirmAsync({
message: 'Do you want to disable these devices on your Apple account as well?',
});
logger.newLine();
const deleteOnApple = nonInteractive
? true
: await toggleConfirmAsync({
message: 'Do you want to disable these devices on your Apple account as well?',
});
if (!deleteOnApple) {
return;
@@ -86,7 +98,7 @@ export default class DeviceDelete extends EasCommand {
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
const context = getRequestContext(ctx);
Log.addNewLineIfNone();
logger.addNewLineIfNone();
const removeAppleSpinner = ora('Disabling devices on Apple').start();
try {
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
@@ -107,18 +119,21 @@ export default class DeviceDelete extends EasCommand {
}
async askAndRemoveFromExpoAsync(
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
): Promise<boolean> {
Log.warn(
logger.warn(
`You are about to remove the Apple device${
chosenDevices.length > 1 ? 's' : ''
} listed above from your Expo account.`
);
Log.newLine();
logger.newLine();
const confirmed = await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
const confirmed = nonInteractive
? true
: await toggleConfirmAsync({
message: 'Are you sure you wish to proceed?',
});
if (confirmed) {
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
@@ -137,49 +152,55 @@ export default class DeviceDelete extends EasCommand {
}
logChosenDevices(
{ logger }: CommandContext,
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
appleTeamName: Maybe<string> | undefined,
appleTeamIdentifier: string
): void {
Log.addNewLineIfNone();
logger.addNewLineIfNone();
chosenDevices.forEach(device => {
Log.log(
logger.log(
formatDevice(device, {
appleTeamName,
appleTeamIdentifier: appleTeamIdentifier!,
})
);
Log.newLine();
logger.newLine();
});
}
async chooseDevicesToDeleteAsync(
{ nonInteractive, logger }: CommandContext,
appleDevices: AppleDeviceQueryResult[],
udids: string[]
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
Log.newLine();
logger.newLine();
if (udids) {
udids.forEach(udid => {
const foundDevice = appleDevices.find(device => device.identifier === udid);
if (foundDevice) {
chosenDevices.push(foundDevice);
} else {
Log.warn(`No device found with UDID ${udid}.`);
logger.warn(`No device found with UDID ${udid}.`);
}
});
}
if (chosenDevices.length === 0) {
Log.addNewLineIfNone();
if (nonInteractive) {
throw new Error('Must supply `udid` paramter in non-interactive mode');
}
logger.addNewLineIfNone();
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
Log.newLine();
logger.newLine();
}
return chosenDevices;
}
async getDevicesForTeamAsync(
{ ora }: CommandContext,
accountName: string,
appleTeamIdentifier: string
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
@@ -209,7 +230,10 @@ export default class DeviceDelete extends EasCommand {
}
}
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
async askForAppleTeamAsync(
{ prompts: { promptAsync }, ora }: CommandContext,
accountName: string
): Promise<string | undefined> {
const teamSpinner = ora().start('Fetching the list of teams for the project…');
try {
@@ -18,6 +18,7 @@ export default class MetadataPull extends EasCommand {
static override flags = {
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
@@ -16,6 +16,7 @@ export default class MetadataPush extends EasCommand {
static override flags = {
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
+3 -2
View File
@@ -1,4 +1,4 @@
import { EasJsonReader } from '@expo/eas-json';
import { EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -56,6 +56,7 @@ export default class Submit extends EasCommand {
options: ['android', 'ios', 'all'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
@@ -106,7 +107,7 @@ export default class Submit extends EasCommand {
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
easJsonReader: new EasJsonReader(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms,
profileName: flagsWithPlatform.profile,
});
@@ -2,10 +2,9 @@ import { Platform } from '@expo/eas-build-job';
import {
CredentialsSource,
DistributionType,
EasJsonReader,
EasJsonAccessor,
IosEnterpriseProvisioning,
} from '@expo/eas-json';
import fs from 'fs-extra';
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
@@ -129,10 +128,16 @@ export default class IosCredentialsProvider {
private async disablePushNotificationsSetupInEasJsonAsync(
ctx: CredentialsContext
): Promise<void> {
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
const easJson = await fs.readJSON(easJsonPath);
easJson.cli = { ...easJson?.cli, promptToConfigurePushNotifications: false };
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
const easJsonAccessor = new EasJsonAccessor(ctx.projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
...easJsonRawObject?.cli,
promptToConfigurePushNotifications: false,
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json');
}
@@ -1,25 +1,29 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader } from '@expo/eas-json';
import { BuildProfile, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import Log from '../../log';
import { promptAsync } from '../../prompts';
export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonReader: EasJsonReader;
private easJsonAccessor: EasJsonAccessor;
constructor(projectDir: string, private platform: T) {
this.easJsonReader = new EasJsonReader(projectDir);
this.easJsonAccessor = new EasJsonAccessor(projectDir);
}
async runAsync(): Promise<BuildProfile<T>> {
const profileName = await this.getProfileNameFromEasConfigAsync();
const easConfig = await this.easJsonReader.getBuildProfileAsync<T>(this.platform, profileName);
const easConfig = await EasJsonUtils.getBuildProfileAsync<T>(
this.easJsonAccessor,
this.platform,
profileName
);
Log.succeed(`Using build profile: ${profileName}`);
return easConfig;
}
async getProfileNameFromEasConfigAsync(): Promise<string> {
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
const buildProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(this.easJsonAccessor);
if (buildProfileNames.length === 0) {
throw new Error(
'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details'
+2 -2
View File
@@ -14,7 +14,7 @@ import DeviceCreateAction from './actions/create/action';
import { DeviceManagerContext } from './context';
const CREATE_COMMAND_DESCRIPTION = `This command lets you register your Apple devices (iPhones and iPads) for internal distribution of your app.
Internal distribution means that you won't need upload your app archive to App Store / Testflight.
Internal distribution means that you won't need to upload your app archive to App Store / Testflight.
Your app archive (.ipa) will be installable on your equipment as long as you sign your application with an adhoc provisiong profile.
The provisioning profile needs to contain the UDIDs (unique identifiers) of your iPhones and iPads.
@@ -72,7 +72,7 @@ export class AccountResolver {
}
const useProjectAccount = await confirmAsync({
message: `You're inside the project directory. Would you like to use ${chalk.underline(
message: `You're inside the project directory. Would you like to use the ${chalk.underline(
projectAccountName
)} account?`,
});
+38 -31
View File
@@ -9,70 +9,77 @@ type Color = (...text: string[]) => string;
export default class Log {
public static readonly isDebug = boolish('EXPO_DEBUG', false);
public static log(...args: any[]): void {
Log.consoleLog(...args);
constructor(private readonly jsonOutputMode: boolean) {}
public log(...args: any[]): void {
this.consoleLog(...args);
}
public static newLine(): void {
Log.consoleLog();
public newLine(): void {
this.consoleLog();
}
public static addNewLineIfNone(): void {
if (!Log.isLastLineNewLine) {
Log.newLine();
public addNewLineIfNone(): void {
if (!this.isLastLineNewLine) {
this.newLine();
}
}
public static error(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.red));
public error(...args: any[]): void {
this.consoleLog(...this.withTextColor(args, chalk.red));
}
public static warn(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
public warn(...args: any[]): void {
this.consoleLog(...this.withTextColor(args, chalk.yellow));
}
public static debug(...args: any[]): void {
public debug(...args: any[]): void {
if (Log.isDebug) {
Log.consoleLog(...args);
this.consoleLog(...args);
}
}
public static gray(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
public gray(...args: any[]): void {
this.consoleLog(...this.withTextColor(args, chalk.gray));
}
public static warnDeprecatedFlag(flag: string, message: string): void {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
public warnDeprecatedFlag(flag: string, message: string): void {
this.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
public succeed(message: string): void {
this.log(`${chalk.green(logSymbols.success)} ${message}`);
}
public static withTick(...args: any[]): void {
Log.consoleLog(chalk.green(figures.tick), ...args);
public withTick(...args: any[]): void {
this.consoleLog(chalk.green(figures.tick), ...args);
}
private static consoleLog(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.log(...args);
private consoleLog(...args: any[]): void {
this.updateIsLastLineNewLine(args);
if (this.jsonOutputMode) {
// eslint-disable-next-line no-console
console.error(...args);
} else {
// eslint-disable-next-line no-console
console.log(...args);
}
}
private static withTextColor(args: any[], chalkColor: Color): string[] {
private withTextColor(args: any[], chalkColor: Color): string[] {
return args.map(arg => chalkColor(arg));
}
private static isLastLineNewLine = false;
private static updateIsLastLineNewLine(args: any[]): void {
private isLastLineNewLine = false;
private updateIsLastLineNewLine(args: any[]): void {
if (args.length === 0) {
Log.isLastLineNewLine = true;
this.isLastLineNewLine = true;
} else {
const lastArg = args[args.length - 1];
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
Log.isLastLineNewLine = true;
this.isLastLineNewLine = true;
} else {
Log.isLastLineNewLine = false;
this.isLastLineNewLine = false;
}
}
}
+7 -3
View File
@@ -1,7 +1,7 @@
import { App, Session } from '@expo/apple-utils';
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader, SubmitProfile } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils, SubmitProfile } from '@expo/eas-json';
import assert from 'assert';
import { CredentialsContext } from '../credentials/context';
@@ -47,8 +47,12 @@ export async function createMetadataContextAsync(params: {
exp?: ExpoConfig;
profileName?: string;
}): Promise<MetadataContext> {
const easJsonReader = new EasJsonReader(params.projectDir);
const submitProfile = await easJsonReader.getSubmitProfileAsync(Platform.IOS, params.profileName);
const easJsonAccessor = new EasJsonAccessor(params.projectDir);
const submitProfile = await EasJsonUtils.getSubmitProfileAsync(
easJsonAccessor,
Platform.IOS,
params.profileName
);
const exp = params.exp ?? getExpoConfig(params.projectDir);
const user = await ensureLoggedInAsync();
+23 -1
View File
@@ -3,6 +3,28 @@ import oraReal, { Options, Ora } from 'ora';
import Log from './log';
export function nonInteractiveOra(_options?: Options | string): Ora {
const stubOra: Ora = {
text: '',
prefixText: '',
color: 'black',
spinner: 'arc',
indent: 0,
isSpinning: false,
start: () => stubOra,
stop: () => stubOra,
succeed: () => stubOra,
fail: () => stubOra,
warn: () => stubOra,
info: () => stubOra,
stopAndPersist: () => stubOra,
clear: () => stubOra,
render: () => stubOra,
frame: () => '',
};
return stubOra;
}
export { Ora, Options };
// eslint-disable-next-line no-console
@@ -20,7 +42,7 @@ const errorReal = console.error;
* @param options
* @returns
*/
export function ora(options?: Options | string): Ora {
export function interactiveOra(options?: Options | string): Ora {
const inputOptions = typeof options === 'string' ? { text: options } : options ?? {};
const disabled = Log.isDebug || !process.stdin.isTTY;
const spinner = oraReal({
@@ -1,18 +1,16 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { AppVersionSource, EasJson, EasJsonReader } from '@expo/eas-json';
import { AppVersionSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { ProfileData } from '../utils/profiles';
export async function ensureVersionSourceIsRemoteAsync(
projectDir: string,
easJsonReader: EasJsonReader
easJsonAccessor: EasJsonAccessor
): Promise<void> {
const easJsonCliConfig = await easJsonReader.getCliConfigAsync();
const easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
return;
}
@@ -30,10 +28,12 @@ export async function ensureVersionSourceIsRemoteAsync(
throw new Error('Aborting...');
}
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const easJson = await fs.readJSON(easJsonPath);
easJson.cli = { ...easJson?.cli, appVersionSource: AppVersionSource.REMOTE };
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = { ...easJsonRawObject?.cli, appVersionSource: AppVersionSource.REMOTE };
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json');
}
+120 -79
View File
@@ -9,87 +9,128 @@ export interface ExpoChoice<T> extends Choice {
type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>;
export async function promptAsync<T extends string = string>(
questions: Question<T> | Question<T>[],
options: Options = {}
): Promise<Answers<T>> {
if (!process.stdin.isTTY && !global.test) {
throw new Error('Input is required, but stdin is not readable.');
export interface Prompts {
promptAsync<T extends string = string>(
questions: Question<T> | Question<T>[],
options?: Options
): Promise<Answers<T>>;
confirmAsync(question: NamelessQuestion, options?: Options): Promise<boolean>;
selectAsync<T>(message: string, choices: ExpoChoice<T>[], options?: Options): Promise<T>;
toggleConfirmAsync(questions: NamelessQuestion, options?: Options): Promise<boolean>;
pressAnyKeyToContinueAsync(): Promise<void>;
}
export class NonInteractivePrompts implements Prompts {
private static errorMessage =
'Command can not be run as specified in non-interactive mode. Run the command with --help to see arguments and flags required to run in non-interactive mode.';
async promptAsync<T extends string = string>(
_questions: prompts.PromptObject<T> | prompts.PromptObject<T>[],
_options: prompts.Options
): Promise<prompts.Answers<T>> {
throw new Error(NonInteractivePrompts.errorMessage);
}
async confirmAsync(
_question: NamelessQuestion,
_options?: prompts.Options | undefined
): Promise<boolean> {
throw new Error(NonInteractivePrompts.errorMessage);
}
async selectAsync<T>(
_message: string,
_choices: ExpoChoice<T>[],
_options?: prompts.Options | undefined
): Promise<T> {
throw new Error(NonInteractivePrompts.errorMessage);
}
async toggleConfirmAsync(
_questions: NamelessQuestion,
_options?: prompts.Options | undefined
): Promise<boolean> {
throw new Error(NonInteractivePrompts.errorMessage);
}
async pressAnyKeyToContinueAsync(): Promise<void> {
throw new Error(NonInteractivePrompts.errorMessage);
}
return await prompts<T>(questions, {
onCancel() {
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
},
...options,
});
}
export async function confirmAsync(
question: NamelessQuestion,
options?: Options
): Promise<boolean> {
const { value } = await promptAsync(
{
initial: true,
...question,
name: 'value',
type: 'confirm',
},
options
);
return value;
}
export async function selectAsync<T>(
message: string,
choices: ExpoChoice<T>[],
options?: Options
): Promise<T> {
const { value } = await promptAsync(
{
message,
choices,
name: 'value',
type: 'select',
},
options
);
return value ?? null;
}
/**
* Create a more dynamic yes/no confirmation that can be cancelled.
*
* @param questions
* @param options
*/
export async function toggleConfirmAsync(
questions: NamelessQuestion,
options?: Options
): Promise<boolean> {
const { value } = await promptAsync(
{
active: 'yes',
inactive: 'no',
...questions,
name: 'value',
type: 'toggle',
},
options
);
return value ?? null;
}
export async function pressAnyKeyToContinueAsync(): Promise<void> {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
await new Promise<void>(res => {
process.stdin.on('data', key => {
if (String(key) === '\u0003') {
process.exit(constants.signals.SIGINT + 128); // ctrl-c
}
res();
export class InteractivePrompts implements Prompts {
async promptAsync<T extends string = string>(
questions: Question<T> | Question<T>[],
options: Options = {}
): Promise<Answers<T>> {
if (!process.stdin.isTTY && !global.test) {
throw new Error('Input is required, but stdin is not readable.');
}
return await prompts<T>(questions, {
onCancel() {
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
},
...options,
});
});
}
async confirmAsync(question: NamelessQuestion, options?: Options): Promise<boolean> {
const { value } = await this.promptAsync(
{
initial: true,
...question,
name: 'value',
type: 'confirm',
},
options
);
return value;
}
async selectAsync<T>(message: string, choices: ExpoChoice<T>[], options?: Options): Promise<T> {
const { value } = await this.promptAsync(
{
message,
choices,
name: 'value',
type: 'select',
},
options
);
return value ?? null;
}
/**
* Create a more dynamic yes/no confirmation that can be cancelled.
*
* @param questions
* @param options
*/
async toggleConfirmAsync(questions: NamelessQuestion, options?: Options): Promise<boolean> {
const { value } = await this.promptAsync(
{
active: 'yes',
inactive: 'no',
...questions,
name: 'value',
type: 'toggle',
},
options
);
return value ?? null;
}
async pressAnyKeyToContinueAsync(): Promise<void> {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
await new Promise<void>(res => {
process.stdin.on('data', key => {
if (String(key) === '\u0003') {
process.exit(constants.signals.SIGINT + 128); // ctrl-c
}
res();
});
});
}
}
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonReader, errors } from '@expo/eas-json';
import { EasJsonAccessor, EasJsonUtils, errors } from '@expo/eas-json';
import { selectAsync } from '../../prompts';
import { getProfilesAsync } from '../profiles';
@@ -8,19 +8,18 @@ jest.mock('../../prompts');
jest.mock('@expo/eas-json', () => {
const actual = jest.requireActual('@expo/eas-json');
const EasJsonReaderMock = jest.fn();
EasJsonReaderMock.prototype = {
const EasJsonUtilsMock = {
getBuildProfileAsync: jest.fn(),
getBuildProfileNamesAsync: jest.fn(),
};
return {
...actual,
EasJsonReader: EasJsonReaderMock,
EasJsonUtils: EasJsonUtilsMock,
};
});
const getBuildProfileAsync = jest.spyOn(EasJsonReader.prototype, 'getBuildProfileAsync');
const getBuildProfileNamesAsync = jest.spyOn(EasJsonReader.prototype, 'getBuildProfileNamesAsync');
const getBuildProfileAsync = jest.spyOn(EasJsonUtils, 'getBuildProfileAsync');
const getBuildProfileNamesAsync = jest.spyOn(EasJsonUtils, 'getBuildProfileNamesAsync');
describe(getProfilesAsync, () => {
afterEach(() => {
@@ -30,8 +29,9 @@ describe(getProfilesAsync, () => {
});
it('defaults to production profile', async () => {
const easJsonAccessor = new EasJsonAccessor('/fake');
const result = await getProfilesAsync({
easJsonReader: new EasJsonReader('/fake'),
easJsonAccessor,
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
type: 'build',
@@ -39,8 +39,8 @@ describe(getProfilesAsync, () => {
expect(result[0].profileName).toBe('production');
expect(result[1].profileName).toBe('production');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, undefined);
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, undefined);
expect(getBuildProfileAsync).toBeCalledWith(easJsonAccessor, Platform.ANDROID, undefined);
expect(getBuildProfileAsync).toBeCalledWith(easJsonAccessor, Platform.IOS, undefined);
});
it('throws an error if there are no profiles in eas.json', async () => {
@@ -51,7 +51,7 @@ describe(getProfilesAsync, () => {
await expect(
getProfilesAsync({
easJsonReader: new EasJsonReader('/fake'),
easJsonAccessor: new EasJsonAccessor('/fake'),
platforms: [Platform.ANDROID],
profileName: undefined,
type: 'build',
@@ -60,8 +60,9 @@ describe(getProfilesAsync, () => {
});
it('gets a specific profile', async () => {
const easJsonAccessor = new EasJsonAccessor('/fake');
const result = await getProfilesAsync({
easJsonReader: new EasJsonReader('/fake'),
easJsonAccessor,
platforms: [Platform.ANDROID, Platform.IOS],
profileName: 'custom-profile',
type: 'build',
@@ -69,8 +70,12 @@ describe(getProfilesAsync, () => {
expect(result[0].profileName).toBe('custom-profile');
expect(result[1].profileName).toBe('custom-profile');
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
expect(getBuildProfileAsync).toBeCalledWith(
easJsonAccessor,
Platform.ANDROID,
'custom-profile'
);
expect(getBuildProfileAsync).toBeCalledWith(easJsonAccessor, Platform.IOS, 'custom-profile');
});
it('throws validation error if eas.json is invalid', async () => {
@@ -80,7 +85,7 @@ describe(getProfilesAsync, () => {
await expect(
getProfilesAsync({
easJsonReader: new EasJsonReader('/fake'),
easJsonAccessor: new EasJsonAccessor('/fake'),
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
type: 'build',
-39
View File
@@ -1,39 +0,0 @@
import assert from 'assert';
import Log from '../log';
let stdoutWrite: NodeJS.WriteStream['write'] | undefined;
export function enableJsonOutput(): void {
if (stdoutWrite) {
return;
}
stdoutWrite = process.stdout.write;
process.stdout.write = process.stderr.write.bind(process.stderr);
}
export function printJsonOnlyOutput(value: object): void {
assert(stdoutWrite, 'this should only be called with --json flag');
try {
process.stdout.write = stdoutWrite;
Log.log(JSON.stringify(sanitizeValue(value), null, 2));
} finally {
process.stdout.write = process.stderr.write.bind(process.stderr);
}
}
function sanitizeValue(value: any): unknown {
if (Array.isArray(value)) {
return value.map(val => sanitizeValue(val));
} else if (value && typeof value === 'object') {
const result: Record<string, any> = {};
Object.keys(value).forEach(key => {
if (key !== '__typename' && value[key] !== null) {
result[key] = sanitizeValue(value[key]);
}
});
return result;
} else {
return value;
}
}
+22 -8
View File
@@ -1,5 +1,11 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader, ProfileType, SubmitProfile } from '@expo/eas-json';
import {
BuildProfile,
EasJsonAccessor,
EasJsonUtils,
ProfileType,
SubmitProfile,
} from '@expo/eas-json';
type EasProfile<T extends ProfileType> = T extends 'build'
? BuildProfile<Platform>
@@ -12,19 +18,19 @@ export type ProfileData<T extends ProfileType> = {
};
export async function getProfilesAsync<T extends ProfileType>({
easJsonReader,
easJsonAccessor,
platforms,
profileName,
type,
}: {
easJsonReader: EasJsonReader;
easJsonAccessor: EasJsonAccessor;
platforms: Platform[];
profileName?: string;
type: T;
}): Promise<ProfileData<T>[]> {
const results = platforms.map(async function (platform) {
const profile = await readProfileAsync({
easJsonReader,
easJsonAccessor,
platform,
type,
profileName,
@@ -40,19 +46,27 @@ export async function getProfilesAsync<T extends ProfileType>({
}
async function readProfileAsync<T extends ProfileType>({
easJsonReader,
easJsonAccessor,
platform,
type,
profileName,
}: {
easJsonReader: EasJsonReader;
easJsonAccessor: EasJsonAccessor;
platform: Platform;
type: T;
profileName?: string;
}): Promise<EasProfile<T>> {
if (type === 'build') {
return (await easJsonReader.getBuildProfileAsync(platform, profileName)) as EasProfile<T>;
return (await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
profileName
)) as EasProfile<T>;
} else {
return (await easJsonReader.getSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
return (await EasJsonUtils.getSubmitProfileAsync(
easJsonAccessor,
platform,
profileName
)) as EasProfile<T>;
}
}
+3 -1
View File
@@ -5,17 +5,19 @@
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "0.2.90",
"@expo/json-file": "8.2.36",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
"golden-fleece": "1.0.9",
"joi": "17.6.0",
"log-symbols": "4.1.0",
"semver": "7.3.7",
"tslib": "2.4.0"
},
"devDependencies": {
"@types/babel__code-frame": "7.0.3",
"@types/fs-extra": "9.0.13",
"memfs": "3.4.7",
"rimraf": "3.0.2",
@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EasJsonAccessor patching JSON file 1`] = `
"{
\\"build\\": {
\\"production\\": {
\\"env\\": {
\\"TEST\\": \\"123\\",
\\"ABC\\": \\"456\\"
}
}
}
}
"
`;
exports[`EasJsonAccessor patching JSON5 file (preserves comments) 1`] = `
"{
\\"build\\": {
\\"production\\": {
// comment
\\"env\\": {
\\"TEST\\": \\"123\\",
ABC: \\"456\\"
}
}
}
}
"
`;
@@ -0,0 +1,72 @@
import fs from 'fs-extra';
import { vol } from 'memfs';
import path from 'path';
import { EasJsonAccessor } from '../accessor';
const fixturesDir = path.join(__dirname, 'fixtures');
const fakeAppPath = '/fake/path/app';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
beforeEach(async () => {
vol.reset();
});
describe(EasJsonAccessor, () => {
test('patching JSON file', async () => {
vol.fromJSON(
{
'./eas.json': await fsReal.readFile(path.join(fixturesDir, 'eas-json.json'), 'utf-8'),
},
fakeAppPath
);
const accessor = new EasJsonAccessor(fakeAppPath);
await accessor.readAsync();
accessor.patch(o => {
o.build.production.env.ABC = '456';
return o;
});
await accessor.writeAsync();
const newEasJsonContents = await fs.readFile(path.join(fakeAppPath, 'eas.json'), 'utf-8');
expect(newEasJsonContents).toMatchSnapshot();
});
test('patching JSON5 file (preserves comments)', async () => {
vol.fromJSON(
{
'./eas.json': await fsReal.readFile(path.join(fixturesDir, 'eas-json5.json'), 'utf-8'),
},
fakeAppPath
);
const accessor = new EasJsonAccessor(fakeAppPath);
await accessor.readAsync();
accessor.patch(o => {
o.build.production.env.ABC = '456';
return o;
});
await accessor.writeAsync();
const newEasJsonContents = await fs.readFile(path.join(fakeAppPath, 'eas.json'), 'utf-8');
expect(newEasJsonContents).toMatchSnapshot();
});
test('reading invalid JSON5 object', async () => {
vol.fromJSON(
{
'./eas.json': await fsReal.readFile(
path.join(fixturesDir, 'eas-invalid-json5.json'),
'utf-8'
),
},
fakeAppPath
);
const accessor = new EasJsonAccessor(fakeAppPath);
await expect(accessor.readAsync()).rejects.toThrow(/Found invalid JSON in eas.json/);
});
});
@@ -2,7 +2,8 @@ import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { EasJsonReader } from '../reader';
import { EasJsonAccessor } from '../accessor';
import { EasJsonUtils } from '../utils';
jest.mock('fs');
@@ -18,9 +19,13 @@ test('minimal valid eas.json for both platforms', async () => {
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
distribution: 'store',
@@ -46,9 +51,13 @@ test('valid eas.json for development client builds', async () => {
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'debug');
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'debug'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
@@ -72,9 +81,13 @@ test(`valid eas.json with autoIncrement flag at build profile root`, async () =>
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
autoIncrement: true,
credentialsSource: 'remote',
@@ -97,8 +110,8 @@ test('valid profile for internal distribution on Android', async () => {
},
});
const reader = new EasJsonReader('/project');
const profile = await reader.getBuildProfileAsync(Platform.ANDROID, 'internal');
const accessor = new EasJsonAccessor('/project');
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'internal');
expect(profile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
@@ -119,9 +132,13 @@ test('valid profile extending other profile', async () => {
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'extension'
);
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
@@ -158,10 +175,18 @@ test('valid profile extending other profile with platform specific envs', async
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'extension'
);
const extendedIosProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.IOS,
'extension'
);
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
@@ -212,10 +237,18 @@ test('valid profile extending other profile with platform specific caching', asy
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'extension'
);
const extendedIosProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.IOS,
'extension'
);
expect(baseProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
@@ -248,8 +281,8 @@ test('valid eas.json with missing profile', async () => {
},
});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'debug');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
});
@@ -260,8 +293,8 @@ test('invalid eas.json when using wrong buildType', async () => {
},
});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.production.android.buildType" must be one of [apk, app-bundle]]'
);
@@ -270,8 +303,8 @@ test('invalid eas.json when using wrong buildType', async () => {
test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
});
@@ -282,8 +315,8 @@ test('invalid semver value', async () => {
},
});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(
'eas.json is not valid [ValidationError: "build.production.node" failed custom validation because alpha is not a valid version]'
);
@@ -296,8 +329,8 @@ test('invalid release channel', async () => {
},
});
const reader = new EasJsonReader('/project');
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
});
@@ -309,7 +342,7 @@ test('get profile names', async () => {
},
});
const reader = new EasJsonReader('/project');
const allProfileNames = await reader.getBuildProfileNamesAsync();
const accessor = new EasJsonAccessor('/project');
const allProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(accessor);
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
});
@@ -0,0 +1,10 @@
{
"build": {
"production": {
"env": {
"TEST": "123",
THIS IS BROKEN: "456",
}
}
}
}
@@ -0,0 +1,9 @@
{
"build": {
"production": {
"env": {
"TEST": "123"
}
}
}
}
@@ -0,0 +1,10 @@
{
"build": {
"production": {
// comment
"env": {
"TEST": "123"
}
}
}
}
@@ -2,7 +2,8 @@ import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { EasJsonReader } from '../reader';
import { EasJsonAccessor } from '../accessor';
import { EasJsonUtils } from '../utils';
jest.mock('fs');
@@ -18,9 +19,13 @@ test('minimal allowed eas.json for both platforms', async () => {
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
changesNotSentForReview: false,
@@ -45,8 +50,12 @@ test('android config with all required values', async () => {
},
});
const reader = new EasJsonReader('/project');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -71,8 +80,12 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
try {
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
const reader = new EasJsonReader('/project');
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
const accessor = new EasJsonAccessor('/project');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
serviceAccountKeyPath: './path.json',
@@ -101,8 +114,8 @@ test('ios config with all required values', async () => {
},
});
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
expect(iosProfile).toEqual({
appleId: '[email protected]',
@@ -135,8 +148,8 @@ test('ios config with ascApiKey fields set to env var', async () => {
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
const reader = new EasJsonReader('/project');
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'release');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
expect(iosProfile).toEqual({
appleId: '[email protected]',
@@ -176,9 +189,13 @@ test('valid profile extending other profile', async () => {
},
});
const reader = new EasJsonReader('/project');
const baseProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'base');
const extendedProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'extension');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'base');
const extendedProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.IOS,
'extension'
);
expect(baseProfile).toEqual({
language: 'en-US',
appleId: '[email protected]',
@@ -216,7 +233,7 @@ test('get profile names', async () => {
},
});
const reader = new EasJsonReader('/project');
const allProfileNames = await reader.getSubmitProfileNamesAsync();
const accessor = new EasJsonAccessor('/project');
const allProfileNames = await EasJsonUtils.getSubmitProfileNamesAsync(accessor);
expect(allProfileNames.sort()).toEqual(['production', 'blah'].sort());
});
+109
View File
@@ -0,0 +1,109 @@
import { codeFrameColumns } from '@babel/code-frame';
import assert from 'assert';
import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import path from 'path';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { EasJsonSchema } from './schema';
import { EasJson } from './types';
export class EasJsonAccessor {
private easJsonPath: string;
private isJson5 = false;
private easJson: EasJson | undefined;
private easJsonRawContents: string | undefined;
private easJsonRawObject: any | undefined;
private easJsonPatched: boolean = false;
constructor(projectDir: string) {
this.easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
}
public static formatEasJsonPath(projectDir: string): string {
return path.join(projectDir, 'eas.json');
}
public async readAsync(): Promise<EasJson> {
if (this.easJson) {
return this.easJson;
}
const rawJSON = await this.readRawJsonAsync();
const { value, error } = EasJsonSchema.validate(rawJSON, {
allowUnknown: false,
abortEarly: false,
convert: true,
noDefaults: true,
});
if (error) {
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
}
this.easJson = value;
return value;
}
public async writeAsync(): Promise<void> {
if (!this.easJsonPatched) {
return;
}
await fs.writeFile(this.easJsonPath, this.easJsonRawContents);
this.resetState();
}
public patch(fn: (easJsonRawObject: any) => any): void {
assert(
this.easJsonRawContents && this.easJsonRawObject,
'call readAsync/readRawJsonAsync first'
);
this.easJsonRawObject = fn(this.easJsonRawObject);
if (this.isJson5) {
this.easJsonRawContents = fleece.patch(this.easJsonRawContents, this.easJsonRawObject);
} else {
this.easJsonRawContents = `${JSON.stringify(this.easJsonRawObject, null, 2)}\n`;
}
this.easJsonPatched = true;
}
public async readRawJsonAsync(): Promise<any> {
if (!(await fs.pathExists(this.easJsonPath))) {
throw new MissingEasJsonError(
`eas.json could not be found at ${this.easJsonPath}. Learn more at https://expo.fyi/eas-json`
);
}
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
try {
const rawJSON = JSON.parse(this.easJsonRawContents);
this.easJsonRawObject = rawJSON;
return rawJSON;
} catch {
// ignore error, try reading as JSON5
}
try {
const rawJSON = fleece.evaluate(this.easJsonRawContents);
this.easJsonRawObject = rawJSON;
this.isJson5 = true;
return rawJSON;
} catch (originalError: any) {
const err = new Error('Found invalid JSON in eas.json.');
if (originalError.loc) {
const codeFrame = codeFrameColumns(this.easJsonRawContents, { start: originalError.loc });
err.message += `\n${codeFrame}`;
}
throw err;
}
}
private resetState(): void {
this.isJson5 = false;
this.easJson = undefined;
this.easJsonRawContents = undefined;
this.easJsonRawObject = undefined;
this.easJsonPatched = false;
}
}
+2 -1
View File
@@ -9,5 +9,6 @@ export {
IosEnterpriseProvisioning,
IosVersionAutoIncrement,
} from './build/types';
export { EasJsonReader } from './reader';
export { EasJsonAccessor } from './accessor';
export { EasJsonUtils } from './utils';
export * as errors from './errors';
-92
View File
@@ -1,92 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import fs from 'fs-extra';
import path from 'path';
import { resolveBuildProfile } from './build/resolver';
import { BuildProfile } from './build/types';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { EasJsonSchema } from './schema';
import { resolveSubmitProfile } from './submit/resolver';
import { SubmitProfile } from './submit/types';
import { EasJson } from './types';
export class EasJsonReader {
private easJson: EasJson | undefined;
constructor(private projectDir: string) {}
public static formatEasJsonPath(projectDir: string): string {
return path.join(projectDir, 'eas.json');
}
public async readAsync(): Promise<EasJson> {
if (this.easJson) {
return this.easJson;
}
try {
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
if (!(await fs.pathExists(easJsonPath))) {
throw new MissingEasJsonError(
`eas.json could not be found at ${easJsonPath}. Learn more at https://expo.fyi/eas-json`
);
}
const contents = JsonFile.read(easJsonPath);
const { value, error } = EasJsonSchema.validate(contents, {
allowUnknown: false,
abortEarly: false,
convert: true,
noDefaults: true,
});
if (error) {
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
}
this.easJson = value;
return value;
} catch (err: any) {
if (err.code === 'EJSONPARSE') {
err.message = `Found invalid JSON in eas.json. ${err.message}`;
}
throw err;
}
}
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readAsync();
return Object.keys(easJson?.build ?? {});
}
public async getBuildProfileAsync<T extends Platform>(
platform: T,
profileName?: string
): Promise<BuildProfile<T>> {
const easJson = await this.readAsync();
return resolveBuildProfile({ easJson, platform, profileName });
}
public async getCliConfigAsync(): Promise<EasJson['cli'] | null> {
try {
const easJson = await this.readAsync();
return easJson.cli ?? null;
} catch (err: any) {
if (err instanceof MissingEasJsonError) {
return null;
}
throw err;
}
}
public async getSubmitProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readAsync();
return Object.keys(easJson?.submit ?? {});
}
public async getSubmitProfileAsync<T extends Platform>(
platform: T,
profileName?: string
): Promise<SubmitProfile<T>> {
const easJson = await this.readAsync();
return resolveSubmitProfile({ easJson, platform, profileName });
}
}
+51
View File
@@ -0,0 +1,51 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor } from './accessor';
import { resolveBuildProfile } from './build/resolver';
import { BuildProfile } from './build/types';
import { MissingEasJsonError } from './errors';
import { resolveSubmitProfile } from './submit/resolver';
import { SubmitProfile } from './submit/types';
import { EasJson } from './types';
export class EasJsonUtils {
public static async getBuildProfileNamesAsync(accessor: EasJsonAccessor): Promise<string[]> {
const easJson = await accessor.readAsync();
return Object.keys(easJson?.build ?? {});
}
public static async getBuildProfileAsync<T extends Platform>(
accessor: EasJsonAccessor,
platform: T,
profileName?: string
): Promise<BuildProfile<T>> {
const easJson = await accessor.readAsync();
return resolveBuildProfile({ easJson, platform, profileName });
}
public static async getCliConfigAsync(accessor: EasJsonAccessor): Promise<EasJson['cli'] | null> {
try {
const easJson = await accessor.readAsync();
return easJson.cli ?? null;
} catch (err: any) {
if (err instanceof MissingEasJsonError) {
return null;
}
throw err;
}
}
public static async getSubmitProfileNamesAsync(accessor: EasJsonAccessor): Promise<string[]> {
const easJson = await accessor.readAsync();
return Object.keys(easJson?.submit ?? {});
}
public static async getSubmitProfileAsync<T extends Platform>(
accessor: EasJsonAccessor,
platform: T,
profileName?: string
): Promise<SubmitProfile<T>> {
const easJson = await accessor.readAsync();
return resolveSubmitProfile({ easJson, platform, profileName });
}
}
Generated
+17 -7
View File
@@ -47,6 +47,13 @@
dependencies:
node-fetch "^2.6.1"
"@babel/[email protected]", "@babel/code-frame@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
dependencies:
"@babel/highlight" "^7.18.6"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@~7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
@@ -82,13 +89,6 @@
dependencies:
"@babel/highlight" "^7.16.7"
"@babel/code-frame@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
dependencies:
"@babel/highlight" "^7.18.6"
"@babel/compat-data@^7.16.0":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e"
@@ -3689,6 +3689,11 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@types/[email protected]":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.3.tgz#eda94e1b7c9326700a4b69c485ebbc9498a0b63f"
integrity sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==
"@types/babel__core@^7.1.14":
version "7.1.15"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024"
@@ -7199,6 +7204,11 @@ globby@^11.0.2, globby@^11.0.3, globby@^11.0.4:
merge2 "^1.3.0"
slash "^3.0.0"
[email protected]:
version "1.0.9"
resolved "https://registry.yarnpkg.com/golden-fleece/-/golden-fleece-1.0.9.tgz#0c4cbba8b493c8ab4cd4023404317a97785306f3"
integrity sha512-YSwLaGMOgSBx9roJlNLL12c+FRiw7VECphinc6mGucphc/ZxTHgdEz6gmJqH6NOzYEd/yr64hwjom5pZ+tJVpg==
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"