eas-cli
Version:
EAS command line tool
316 lines (315 loc) • 16.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const config_1 = require("@expo/config");
const core_1 = require("@oclif/core");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const nullthrows_1 = tslib_1.__importDefault(require("nullthrows"));
const url_1 = require("../../build/utils/url");
const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
const getProjectIdAsync_1 = require("../../commandUtils/context/contextUtils/getProjectIdAsync");
const flags_1 = require("../../commandUtils/flags");
const generated_1 = require("../../graphql/generated");
const AppMutation_1 = require("../../graphql/mutations/AppMutation");
const AppQuery_1 = require("../../graphql/queries/AppQuery");
const log_1 = tslib_1.__importStar(require("../../log"));
const ora_1 = require("../../ora");
const expoConfig_1 = require("../../project/expoConfig");
const fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1 = require("../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync");
const prompts_1 = require("../../prompts");
class ProjectInit extends EasCommand_1.default {
static description = 'create or link an EAS project';
static aliases = ['init'];
static flags = {
id: core_1.Flags.string({
description: 'ID of the EAS project to link',
}),
force: core_1.Flags.boolean({
description: 'Whether to create a new project/link an existing project without additional prompts or overwrite any existing project ID when running with --id flag',
}),
...flags_1.EASNonInteractiveFlag,
};
static contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectDir,
};
static async saveProjectIdAndLogSuccessAsync(projectDir, projectId) {
await (0, getProjectIdAsync_1.saveProjectIdToAppConfigAsync)(projectDir, projectId);
log_1.default.withTick(`Project successfully linked (ID: ${chalk_1.default.bold(projectId)}) (modified app.json)`);
}
static async modifyExpoConfigAsync(projectDir, modifications) {
let result;
try {
result = await (0, expoConfig_1.createOrModifyExpoConfigAsync)(projectDir, modifications);
}
catch (error) {
if (error instanceof config_1.ConfigError && error.code === 'MODULE_NOT_FOUND') {
log_1.default.warn('Cannot determine which native SDK version your project uses because the module `expo` is not installed.');
return;
}
else {
throw error;
}
}
switch (result.type) {
case 'success':
break;
case 'warn': {
log_1.default.warn();
log_1.default.warn(`Warning: Your project uses dynamic app configuration, and cannot be automatically modified.`);
log_1.default.warn(chalk_1.default.dim('https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'));
log_1.default.warn();
log_1.default.warn(`To complete the setup process, add the following in your ${chalk_1.default.bold((0, config_1.getProjectConfigDescription)(projectDir))}:`);
log_1.default.warn();
log_1.default.warn(chalk_1.default.bold(JSON.stringify(modifications, null, 2)));
log_1.default.warn();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
static async ensureOwnerSlugConsistencyAsync(graphqlClient, projectId, projectDir, { force, nonInteractive }) {
const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir);
const appForProjectId = await AppQuery_1.AppQuery.byIdAsync(graphqlClient, projectId);
const correctOwner = appForProjectId.ownerAccount.name;
const correctSlug = appForProjectId.slug;
if (exp.owner && exp.owner !== correctOwner) {
if (force) {
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
}
else {
const message = `Project owner (${correctOwner}) does not match the value configured in the "owner" field (${exp.owner}).`;
if (nonInteractive) {
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
}
const confirm = await (0, prompts_1.confirmAsync)({
message: `${message}. Do you wish to overwrite it?`,
});
if (!confirm) {
throw new Error('Aborting');
}
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
}
}
else if (!exp.owner) {
await this.modifyExpoConfigAsync(projectDir, { owner: correctOwner });
}
if (exp.slug && exp.slug !== correctSlug) {
if (force) {
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
}
else {
const message = `Project slug (${correctSlug}) does not match the value configured in the "slug" field (${exp.slug}).`;
if (nonInteractive) {
throw new Error(`Project config error: ${message} Use --force flag to overwrite.`);
}
const confirm = await (0, prompts_1.confirmAsync)({
message: `${message}. Do you wish to overwrite it?`,
});
if (!confirm) {
throw new Error('Aborting');
}
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
}
}
else if (!exp.slug) {
await this.modifyExpoConfigAsync(projectDir, { slug: correctSlug });
}
}
static async setExplicitIDAsync(projectId, projectDir, { force, nonInteractive }) {
const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
if (projectId === existingProjectId) {
log_1.default.succeed(`Project already linked (ID: ${chalk_1.default.bold(existingProjectId)})`);
return;
}
if (!existingProjectId) {
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
return;
}
if (projectId !== existingProjectId) {
if (force) {
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
return;
}
if (nonInteractive) {
throw new Error(`Project is already linked to a different ID: ${chalk_1.default.bold(existingProjectId)}. Use --force flag to overwrite.`);
}
const confirm = await (0, prompts_1.confirmAsync)({
message: `Project is already linked to a different ID: ${chalk_1.default.bold(existingProjectId)}. Do you wish to overwrite it?`,
});
if (!confirm) {
throw new Error('Aborting');
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, projectId);
}
}
static async initializeWithExplicitIDAsync(projectId, projectDir, { force, nonInteractive }) {
await this.setExplicitIDAsync(projectId, projectDir, {
force,
nonInteractive,
});
}
static async initializeWithoutExplicitIDAsync(graphqlClient, actor, projectDir, { force, nonInteractive }) {
const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
if (existingProjectId) {
log_1.default.succeed(`Project already linked (ID: ${chalk_1.default.bold(existingProjectId)}). To re-configure, remove the "extra.eas.projectId" field from your app config.`);
return existingProjectId;
}
const allAccounts = actor.accounts;
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(allAccounts
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== generated_1.Role.ViewOnly)
.map(it => it.name));
// if no owner field, ask the user which account they want to use to create/link the project
let accountName = exp.owner;
if (!accountName) {
if (allAccounts.length === 1) {
accountName = allAccounts[0].name;
}
else if (nonInteractive) {
if (!force) {
throw new Error(`There are multiple accounts that you have access to: ${allAccounts
.map(a => a.name)
.join(', ')}. Explicitly set the owner property in your app config or run this command with the --force flag to proceed with a default account: ${allAccounts[0].name}.`);
}
accountName = allAccounts[0].name;
log_1.default.log(`Using default account ${accountName} for non-interactive and force mode`);
}
else {
const choices = ProjectInit.getAccountChoices(actor, accountNamesWhereUserHasSufficientPermissionsToCreateApp);
accountName = (await (0, prompts_1.promptAsync)({
type: 'select',
name: 'account',
message: 'Which account should own this project?',
choices,
})).account.name;
}
}
if (!accountName) {
throw new Error('No account selected for project. Canceling.');
}
const projectName = exp.slug;
const projectFullName = `@${accountName}/${projectName}`;
const existingProjectIdOnServer = await (0, fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1.findProjectIdByAccountNameAndSlugNullableAsync)(graphqlClient, accountName, projectName);
if (existingProjectIdOnServer) {
if (!force) {
if (nonInteractive) {
throw new Error(`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Use --force flag to continue with this project.`);
}
const affirmedLink = await (0, prompts_1.confirmAsync)({
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
});
if (!affirmedLink) {
throw new Error(`Project ID configuration canceled. Re-run the command to select a different account/project.`);
}
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
return existingProjectIdOnServer;
}
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
throw new Error(`You don't have permission to create a new project on the ${accountName} account and no matching project already exists on the account.`);
}
if (!force) {
if (nonInteractive) {
throw new Error(`Project does not exist: ${projectFullName}. Use --force flag to create this project.`);
}
const affirmedCreate = await (0, prompts_1.confirmAsync)({
message: `Would you like to create a project for ${projectFullName}?`,
});
if (!affirmedCreate) {
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
}
}
const projectDashboardUrl = (0, url_1.getProjectDashboardUrl)(accountName, projectName);
const projectLink = (0, log_1.link)(projectDashboardUrl, { text: projectFullName });
const account = (0, nullthrows_1.default)(allAccounts.find(a => a.name === accountName));
const spinner = (0, ora_1.ora)(`Creating ${chalk_1.default.bold(projectFullName)}`).start();
let createdProjectId;
try {
createdProjectId = await AppMutation_1.AppMutation.createAppAsync(graphqlClient, {
accountId: account.id,
projectName,
});
spinner.succeed(`Created ${chalk_1.default.bold(projectLink)}`);
}
catch (err) {
spinner.fail();
throw err;
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, createdProjectId);
return createdProjectId;
}
static getAccountChoices(actor, namesWithSufficientPermissions) {
const allAccounts = actor.accounts;
const sortedAccounts = actor.__typename === 'Robot'
? allAccounts
: [...allAccounts].sort((a, _b) => actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0);
if (actor.__typename !== 'Robot') {
const personalAccount = allAccounts?.find(account => account?.ownerUserActor?.id === actor.id);
const personalAccountChoice = personalAccount
? {
title: personalAccount.name,
value: personalAccount,
description: !namesWithSufficientPermissions.has(personalAccount.name)
? '(Personal) (Viewer Role)'
: '(Personal)',
}
: undefined;
const userAccounts = allAccounts
?.filter(account => account.ownerUserActor && account.name !== actor.username)
.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name)
? '(Team) (Viewer Role)'
: '(Team)',
}));
const organizationAccounts = allAccounts
?.filter(account => account.name !== actor.username && !account.ownerUserActor)
.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name)
? '(Organization) (Viewer Role)'
: '(Organization)',
}));
let choices = [];
if (personalAccountChoice) {
choices = [personalAccountChoice];
}
return [...choices, ...userAccounts, ...organizationAccounts].sort((a, _b) => actor.__typename === 'User' ? (a.value.name === actor.username ? -1 : 1) : 0);
}
return sortedAccounts.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name) ? '(Viewer Role)' : undefined,
}));
}
async runAsync() {
const { flags: { id: idArgument, force, 'non-interactive': nonInteractive }, } = await this.parse(ProjectInit);
const { loggedIn: { actor, graphqlClient }, projectDir, } = await this.getContextAsync(ProjectInit, { nonInteractive });
let idForConsistency;
if (idArgument) {
await ProjectInit.initializeWithExplicitIDAsync(idArgument, projectDir, {
force,
nonInteractive,
});
idForConsistency = idArgument;
}
else {
idForConsistency = await ProjectInit.initializeWithoutExplicitIDAsync(graphqlClient, actor, projectDir, {
force,
nonInteractive,
});
}
await ProjectInit.ensureOwnerSlugConsistencyAsync(graphqlClient, idForConsistency, projectDir, {
force,
nonInteractive,
});
}
}
exports.default = ProjectInit;
;