UNPKG

eas-cli

Version:
316 lines (315 loc) 16.1 kB
"use strict"; 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;