mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9147ae631c | ||
|
|
9c09cb9034 | ||
|
|
e52d14586f | ||
|
|
0f7646868f | ||
|
|
6fe4511280 | ||
|
|
efc1551d62 |
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.',
|
||||
}),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
+68
-35
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-17
@@ -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());
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user