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 | |
|---|---|---|---|
|
|
750c99184f | ||
|
|
2bd628be1a | ||
|
|
3a707229aa | ||
|
|
5964b4ebe1 | ||
|
|
c11fa806c1 | ||
|
|
e9f1b64285 |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
@@ -8,22 +9,53 @@ import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/cr
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester, robot } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { Role } from '../../../graphql/generated';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
|
||||
import {
|
||||
installDependenciesAsync,
|
||||
promptForPackageManagerAsync,
|
||||
} from '../../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../../onboarding/runCommand';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
createOrModifyExpoConfigAsync,
|
||||
getPrivateExpoConfigAsync,
|
||||
} from '../../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { Actor } from '../../../user/User';
|
||||
import New from '../new';
|
||||
|
||||
// Test helper types
|
||||
interface MockExpoConfig extends Partial<ExpoConfig> {
|
||||
name: string;
|
||||
slug: string;
|
||||
owner?: string;
|
||||
extra?: {
|
||||
eas?: {
|
||||
projectId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface MockConfigResult {
|
||||
type: 'success' | 'warn' | 'fail';
|
||||
message?: string;
|
||||
config: ExpoConfig | null;
|
||||
}
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/installDependencies');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../project/expoConfig');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
@@ -82,8 +114,14 @@ describe(New.name, () => {
|
||||
const commandOptions = { root: process.cwd() } as any;
|
||||
|
||||
describe('successful project creation', () => {
|
||||
// Create a single-account version of jester for basic tests
|
||||
const singleAccountJester = {
|
||||
...jester,
|
||||
accounts: [jester.accounts[0]],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoggedInContext(jester);
|
||||
mockLoggedInContext(singleAccountJester);
|
||||
mockFileSystem(targetProjectDir);
|
||||
|
||||
// Mock git operations
|
||||
@@ -99,6 +137,19 @@ describe(New.name, () => {
|
||||
|
||||
// Mock fs operations
|
||||
(fs.remove as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Mock project configuration
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
} satisfies MockExpoConfig);
|
||||
|
||||
// Mock project initialization
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('test-project-id');
|
||||
jest
|
||||
.mocked(createOrModifyExpoConfigAsync)
|
||||
.mockResolvedValue({ type: 'success', config: null } satisfies MockConfigResult);
|
||||
});
|
||||
|
||||
it('creates a new project with SSH clone method', async () => {
|
||||
@@ -141,6 +192,25 @@ describe(New.name, () => {
|
||||
command: 'git',
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
});
|
||||
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledTimes(1);
|
||||
const createAppCall = jest.mocked(AppMutation.createAppAsync).mock.calls[0];
|
||||
expect(createAppCall[1]).toEqual({
|
||||
accountId: singleAccountJester.accounts[0].id,
|
||||
projectName: 'jester-app',
|
||||
});
|
||||
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
targetProjectDir,
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({
|
||||
eas: expect.objectContaining({
|
||||
projectId: 'test-project-id',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a new project with HTTPS clone method when SSH fails', async () => {
|
||||
@@ -158,7 +228,6 @@ describe(New.name, () => {
|
||||
});
|
||||
|
||||
it('logs appropriate messages during project creation', async () => {
|
||||
mockLoggedInContext(jester);
|
||||
mockFileSystem(targetProjectDir);
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
|
||||
@@ -269,6 +338,229 @@ describe(New.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('project initialization variants', () => {
|
||||
beforeEach(() => {
|
||||
mockLoggedInContext(jester);
|
||||
mockFileSystem(targetProjectDir);
|
||||
|
||||
// Mock git operations
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir,
|
||||
});
|
||||
jest.mocked(installDependenciesAsync).mockResolvedValue();
|
||||
jest.mocked(runCommandAsync).mockResolvedValue();
|
||||
|
||||
// Mock package manager selection (default to npm)
|
||||
jest.mocked(promptForPackageManagerAsync).mockResolvedValue('npm');
|
||||
|
||||
// Mock fs operations
|
||||
(fs.remove as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Default project configuration (can be overridden in individual tests)
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
} satisfies MockExpoConfig);
|
||||
|
||||
// Default project initialization mocks (can be overridden in individual tests)
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('test-project-id');
|
||||
jest
|
||||
.mocked(createOrModifyExpoConfigAsync)
|
||||
.mockResolvedValue({ type: 'success', config: null } satisfies MockConfigResult);
|
||||
|
||||
jest.mocked(promptAsync).mockResolvedValue({
|
||||
account: { name: jester.accounts[0].name },
|
||||
});
|
||||
});
|
||||
|
||||
it('links existing project on server when user confirms', async () => {
|
||||
const existingProjectId = 'existing-server-project-id';
|
||||
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockResolvedValue(existingProjectId);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
await command.run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message: `Existing project found: @${jester.accounts[0].name}/jester-app (ID: ${existingProjectId}). Link this project?`,
|
||||
});
|
||||
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
targetProjectDir,
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({
|
||||
eas: expect.objectContaining({
|
||||
projectId: existingProjectId,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
|
||||
// Should not create a new project since existing one was linked
|
||||
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws error when user declines linking existing project', async () => {
|
||||
const existingProjectId = 'existing-server-project-id';
|
||||
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockResolvedValue(existingProjectId);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
|
||||
await expect(command.run()).rejects.toThrow(
|
||||
'Project ID configuration canceled. Re-run the command to select a different account/project.'
|
||||
);
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prompts for account selection when multiple accounts', async () => {
|
||||
const multipleAccountsActor = {
|
||||
...jester,
|
||||
accounts: [
|
||||
{ ...jester.accounts[0], name: 'account1' },
|
||||
{ ...jester.accounts[0], name: 'account2' },
|
||||
],
|
||||
};
|
||||
|
||||
mockLoggedInContext(multipleAccountsActor);
|
||||
|
||||
jest.mocked(promptAsync).mockResolvedValueOnce({
|
||||
account: { name: 'account2' },
|
||||
});
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
await command.run();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledWith({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices: expect.any(Array),
|
||||
});
|
||||
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledTimes(1);
|
||||
const createAppCall = jest.mocked(AppMutation.createAppAsync).mock.calls[0];
|
||||
expect(createAppCall[1]).toEqual({
|
||||
accountId: jester.accounts[0].id, // Both accounts have same ID since we spread jester.accounts[0]
|
||||
projectName: 'jester-app',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses single account automatically when only one account available', async () => {
|
||||
const singleAccountUser = {
|
||||
...jester,
|
||||
accounts: [jester.accounts[0]],
|
||||
};
|
||||
|
||||
mockLoggedInContext(singleAccountUser);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
await command.run();
|
||||
|
||||
// Should complete the full flow
|
||||
expect(runGitCloneAsync).toHaveBeenCalled();
|
||||
expect(installDependenciesAsync).toHaveBeenCalled();
|
||||
|
||||
// Should not prompt for account selection when only one account
|
||||
expect(promptAsync).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
})
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledTimes(1);
|
||||
const createAppCall = jest.mocked(AppMutation.createAppAsync).mock.calls[0];
|
||||
expect(createAppCall[1]).toEqual({
|
||||
accountId: jester.accounts[0].id,
|
||||
projectName: 'jester-app',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when user has insufficient permissions', async () => {
|
||||
const insufficientPermissionsActor = {
|
||||
...jester,
|
||||
accounts: [
|
||||
{
|
||||
...jester.accounts[0],
|
||||
users: [{ role: Role.ViewOnly, actor: { id: jester.id } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockLoggedInContext(insufficientPermissionsActor);
|
||||
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
} satisfies MockExpoConfig);
|
||||
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
|
||||
await expect(command.run()).rejects.toThrow(
|
||||
`You don't have permission to create a new project on the ${insufficientPermissionsActor.accounts[0].name} account and no matching project already exists on the account.`
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves existing extra config when saving project ID', async () => {
|
||||
const existingExtra = { someField: 'someValue', eas: { otherField: 'otherValue' } };
|
||||
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
extra: existingExtra,
|
||||
} satisfies MockExpoConfig);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
await command.run();
|
||||
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
targetProjectDir,
|
||||
{
|
||||
extra: {
|
||||
...existingExtra,
|
||||
eas: { ...existingExtra.eas, projectId: 'test-project-id' },
|
||||
},
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('handles config modification failure gracefully', async () => {
|
||||
const errorMessage = 'Failed to modify config';
|
||||
|
||||
jest.mocked(createOrModifyExpoConfigAsync).mockResolvedValue({
|
||||
type: 'fail',
|
||||
message: errorMessage,
|
||||
config: null,
|
||||
} satisfies MockConfigResult);
|
||||
|
||||
const command = new New([targetProjectDir], commandOptions);
|
||||
|
||||
await expect(command.run()).rejects.toThrow(errorMessage);
|
||||
|
||||
// Should still complete git and dependency installation
|
||||
expect(runGitCloneAsync).toHaveBeenCalled();
|
||||
expect(installDependenciesAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws error for robot users', async () => {
|
||||
mockLoggedInContext(robot);
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import Log, { link } from '../../log';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
|
||||
import {
|
||||
installDependenciesAsync,
|
||||
promptForPackageManagerAsync,
|
||||
} from '../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../onboarding/runCommand';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ora } from '../../ora';
|
||||
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { Choice, confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor, getActorUsername } from '../../user/User';
|
||||
|
||||
export default class New extends EasCommand {
|
||||
static override aliases = ['new'];
|
||||
@@ -33,12 +42,9 @@ export default class New extends EasCommand {
|
||||
} = await this.parse(New);
|
||||
|
||||
const {
|
||||
loggedIn: { actor },
|
||||
loggedIn: { actor, graphqlClient },
|
||||
} = await this.getContextAsync(New, { nonInteractive: false });
|
||||
|
||||
const githubUsername = 'expo';
|
||||
const githubRepositoryName = 'expo-template-default';
|
||||
|
||||
if (actor.__typename === 'Robot') {
|
||||
throw new Error(
|
||||
'This command is not available for robot users. Make sure you are not using a robot token and try again.'
|
||||
@@ -50,6 +56,19 @@ export default class New extends EasCommand {
|
||||
);
|
||||
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
|
||||
Log.newLine();
|
||||
|
||||
const targetProjectDirInput =
|
||||
await this.getTargetProjectDirectoryAsync(targetProjectDirFromArgs);
|
||||
const finalTargetProjectDirectory = await this.cloneTemplateAsync(targetProjectDirInput);
|
||||
await this.installProjectDependenciesAsync(finalTargetProjectDirectory);
|
||||
await this.initializeGitRepositoryAsync(finalTargetProjectDirectory);
|
||||
await this.initializeProjectAsync(graphqlClient, actor, finalTargetProjectDirectory);
|
||||
|
||||
Log.log('🎉 We finished creating your new project.');
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
private async getTargetProjectDirectoryAsync(targetProjectDirFromArgs: string): Promise<string> {
|
||||
Log.log(
|
||||
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
|
||||
);
|
||||
@@ -66,6 +85,13 @@ export default class New extends EasCommand {
|
||||
).targetProjectDir;
|
||||
}
|
||||
|
||||
return targetProjectDirInput;
|
||||
}
|
||||
|
||||
private async cloneTemplateAsync(targetProjectDirInput: string): Promise<string> {
|
||||
const githubUsername = 'expo';
|
||||
const githubRepositoryName = 'expo-template-default';
|
||||
|
||||
Log.log(`📂 Cloning the project to ${targetProjectDirInput}`);
|
||||
Log.newLine();
|
||||
|
||||
@@ -85,6 +111,20 @@ export default class New extends EasCommand {
|
||||
cloneMethod,
|
||||
});
|
||||
|
||||
return finalTargetProjectDirectory;
|
||||
}
|
||||
|
||||
private async installProjectDependenciesAsync(
|
||||
finalTargetProjectDirectory: string
|
||||
): Promise<void> {
|
||||
const packageManager = await promptForPackageManagerAsync();
|
||||
await installDependenciesAsync({
|
||||
projectDir: finalTargetProjectDirectory,
|
||||
packageManager,
|
||||
});
|
||||
}
|
||||
|
||||
private async initializeGitRepositoryAsync(finalTargetProjectDirectory: string): Promise<void> {
|
||||
await fs.remove(path.join(finalTargetProjectDirectory, '.git'));
|
||||
await runCommandAsync({
|
||||
cwd: finalTargetProjectDirectory,
|
||||
@@ -92,19 +132,6 @@ export default class New extends EasCommand {
|
||||
args: ['init'],
|
||||
});
|
||||
Log.log();
|
||||
// TODO - need to create an app
|
||||
// await configureProjectFromBareDefaultExpoTemplateAsync({
|
||||
// app,
|
||||
// vcsClient,
|
||||
// targetDir: finalTargetProjectDirectory,
|
||||
// });
|
||||
|
||||
const packageManager = await promptForPackageManagerAsync();
|
||||
await installDependenciesAsync({
|
||||
projectDir: finalTargetProjectDirectory,
|
||||
packageManager,
|
||||
});
|
||||
|
||||
await runCommandAsync({
|
||||
cwd: finalTargetProjectDirectory,
|
||||
command: 'git',
|
||||
@@ -116,8 +143,134 @@ export default class New extends EasCommand {
|
||||
command: 'git',
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
});
|
||||
}
|
||||
|
||||
Log.log('🎉 We finished creating your new project.');
|
||||
Log.newLine();
|
||||
private async initializeProjectAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
projectDir: string
|
||||
): Promise<string> {
|
||||
const allAccounts = actor.accounts;
|
||||
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(
|
||||
allAccounts
|
||||
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly)
|
||||
.map(it => it.name)
|
||||
);
|
||||
|
||||
let accountName = allAccounts[0].name;
|
||||
if (allAccounts.length > 1) {
|
||||
const choices = this.getAccountChoices(
|
||||
actor,
|
||||
accountNamesWhereUserHasSufficientPermissionsToCreateApp
|
||||
);
|
||||
|
||||
accountName = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices,
|
||||
})
|
||||
).account.name;
|
||||
}
|
||||
|
||||
const projectName = getActorUsername(actor) + '-app';
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
const existingProjectIdOnServer = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
accountName,
|
||||
projectName
|
||||
);
|
||||
|
||||
if (existingProjectIdOnServer) {
|
||||
const affirmedLink = await 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 this.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.`
|
||||
);
|
||||
}
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = link(projectDashboardUrl, { text: projectFullName });
|
||||
|
||||
const account = nullthrows(allAccounts.find(a => a.name === accountName));
|
||||
|
||||
const spinner = ora(`Creating ${chalk.bold(projectFullName)}`).start();
|
||||
let createdProjectId: string;
|
||||
try {
|
||||
createdProjectId = await AppMutation.createAppAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
projectName,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
|
||||
await this.saveProjectIdAndLogSuccessAsync(projectDir, createdProjectId);
|
||||
return createdProjectId;
|
||||
}
|
||||
|
||||
private async saveProjectIdAndLogSuccessAsync(
|
||||
projectDir: string,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true });
|
||||
const result = await createOrModifyExpoConfigAsync(
|
||||
projectDir,
|
||||
{
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
Log.withTick(
|
||||
`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`
|
||||
);
|
||||
break;
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
private getAccountChoices(actor: Actor, namesWithSufficientPermissions: Set<string>): Choice[] {
|
||||
const sortedAccounts = actor.accounts.sort((a, _b) =>
|
||||
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
|
||||
);
|
||||
|
||||
return sortedAccounts.map(account => {
|
||||
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
||||
const accountDisplayName = isPersonalAccount
|
||||
? `${account.name} (personal account)`
|
||||
: account.name;
|
||||
const disabled = !namesWithSufficientPermissions.has(account.name);
|
||||
|
||||
return {
|
||||
title: accountDisplayName,
|
||||
value: { name: account.name },
|
||||
...(disabled && {
|
||||
disabled: true,
|
||||
description:
|
||||
'You do not have the required permissions to create projects on this account.',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user