eas-cli
Version:
EAS command line tool
356 lines (355 loc) • 18.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const eas_build_job_1 = require("@expo/eas-build-job");
const eas_json_1 = require("@expo/eas-json");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const api_1 = require("../../api");
const runBuildAndSubmit_1 = require("../../build/runBuildAndSubmit");
const repository_1 = require("../../build/utils/repository");
const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
const getProjectIdAsync_1 = require("../../commandUtils/context/contextUtils/getProjectIdAsync");
const SetUpBuildCredentialsCommandAction_1 = require("../../credentials/manager/SetUpBuildCredentialsCommandAction");
const generated_1 = require("../../graphql/generated");
const UserPreferencesMutation_1 = require("../../graphql/mutations/UserPreferencesMutation");
const AppQuery_1 = require("../../graphql/queries/AppQuery");
const log_1 = tslib_1.__importStar(require("../../log"));
const git_1 = require("../../onboarding/git");
const installDependencies_1 = require("../../onboarding/installDependencies");
const runCommand_1 = require("../../onboarding/runCommand");
const platform_1 = require("../../platform");
const expoConfig_1 = require("../../project/expoConfig");
const prompts_1 = require("../../prompts");
const easCli_1 = require("../../utils/easCli");
const git_2 = tslib_1.__importDefault(require("../../vcs/clients/git"));
class Onboarding extends EasCommand_1.default {
static aliases = ['init:onboarding', 'onboarding'];
static description = 'continue onboarding process started on the https://expo.new website.';
static flags = {};
static args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
static contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};
async runAsync() {
const { args: { TARGET_PROJECT_DIRECTORY: targetProjectDirInput }, } = await this.parse(Onboarding);
const { loggedIn: { actor, graphqlClient }, analytics, } = await this.getContextAsync(Onboarding, {
nonInteractive: false,
});
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.');
}
if (!actor.preferences.onboarding) {
throw new Error('This command can only be run as part of the onboarding process started on the Expo website. Visit https://expo.new to start a new project.');
}
if (!actor.preferences.onboarding.platform) {
throw new Error('This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected platform). Continue the onboarding process on the Expo website.');
}
if (!actor.preferences.onboarding.environment) {
throw new Error('This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected environment). Continue the onboarding process on the Expo website.');
}
if (!actor.preferences.onboarding.deviceType) {
throw new Error('This command can only be run as part of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the eas init:onboarding command (selected device type). Continue the onboarding process on the Expo website.');
}
if (new Date(actor.preferences.onboarding.lastUsed) < new Date(Date.now() - 1000 * 60 * 60 * 24)) {
log_1.default.warn('It seems like you started an onboarding process, but it has been a while since you last used it. If you want to start a new onboarding process, visit https://expo.new.');
log_1.default.log();
}
const platform = actor.preferences.onboarding.platform === generated_1.AppPlatform.Android
? eas_build_job_1.Platform.ANDROID
: eas_build_job_1.Platform.IOS;
const app = await AppQuery_1.AppQuery.byIdAsync(graphqlClient, actor.preferences.onboarding.appId);
const githubUsername = app.githubRepository
? app.githubRepository.metadata.githubRepoOwnerName
: 'expo';
const githubRepositoryName = app.githubRepository
? app.githubRepository.metadata.githubRepoName
: 'expo-template-default';
log_1.default.log(`👋 Welcome to Expo, ${actor.username}!`);
log_1.default.log();
log_1.default.log('✨ We will continue your onboarding process in EAS CLI');
log_1.default.log();
log_1.default.log(`🚚 Let's start by cloning ${app.githubRepository
? `your project (${githubUsername}/${githubRepositoryName})`
: `default Expo template project (${githubUsername}/${githubRepositoryName})`} from GitHub and installing dependencies.`);
log_1.default.log();
let initialTargetProjectDirectory;
if (targetProjectDirInput) {
initialTargetProjectDirectory = targetProjectDirInput;
log_1.default.log(`📂 Cloning the project to ${initialTargetProjectDirectory}`);
}
else {
const { selectedTargetProjectDirectory } = await (0, prompts_1.promptAsync)({
type: 'text',
name: 'selectedTargetProjectDirectory',
message: app.githubRepository
? '📂 Where would you like to clone the project to?'
: '📂 Where would you like to create your new project directory?',
initial: app.githubRepository
? path_1.default.join(process.cwd(), githubRepositoryName)
: path_1.default.join(process.cwd(), `${actor.username}-first-project`),
});
initialTargetProjectDirectory = selectedTargetProjectDirectory;
}
log_1.default.log();
const cloneMethod = (await (0, git_1.canAccessRepositoryUsingSshAsync)({
githubUsername,
githubRepositoryName,
}))
? 'ssh'
: 'https';
log_1.default.log(chalk_1.default.dim(`We detected that ${cloneMethod} is your preffered git clone method`));
log_1.default.log();
const { targetProjectDir: finalTargetProjectDirectory } = await (0, git_1.runGitCloneAsync)({
githubUsername,
githubRepositoryName,
targetProjectDir: initialTargetProjectDirectory,
cloneMethod,
});
const vcsClient = new git_2.default(finalTargetProjectDirectory);
if (!app.githubRepository) {
await fs_extra_1.default.remove(path_1.default.join(finalTargetProjectDirectory, '.git'));
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['init'],
});
log_1.default.log();
await configureProjectFromBareDefaultExpoTemplateAsync({
app,
vcsClient,
targetDir: finalTargetProjectDirectory,
});
}
await (0, installDependencies_1.installDependenciesAsync)({
projectDir: finalTargetProjectDirectory,
});
const exp = await getPrivateExpoConfigWithProjectIdAsync({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
});
const getDynamicProjectConfigFn = getDynamicPrivateProjectConfigGetter({
projectDir: finalTargetProjectDirectory,
graphqlClient,
actor,
});
if (!app.githubRepository) {
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-updates'],
});
log_1.default.log();
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-insights'],
});
log_1.default.log();
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'npx',
args: ['expo', 'install', 'expo-dev-client'],
});
log_1.default.log();
}
await vcsClient.trackFileAsync('package-lock.json');
const shouldSetupCredentials = ((platform === eas_build_job_1.Platform.IOS &&
actor.preferences.onboarding.deviceType === generated_1.OnboardingDeviceType.Device) ||
platform === eas_build_job_1.Platform.ANDROID) &&
actor.preferences.onboarding.environment === generated_1.OnboardingEnvironment.DevBuild;
if (shouldSetupCredentials) {
log_1.default.log('🔑 Now we need to set up build credentials for your project:');
await new SetUpBuildCredentialsCommandAction_1.SetUpBuildCredentialsCommandAction(actor, graphqlClient, vcsClient, analytics, exp, getDynamicProjectConfigFn, platform, actor.preferences.onboarding.deviceType === generated_1.OnboardingDeviceType.Simulator
? 'development-simulator'
: 'development', finalTargetProjectDirectory).runAsync();
}
if (app.githubRepository && (await vcsClient.hasUncommittedChangesAsync())) {
log_1.default.log('📦 We will now commit the changes made by the configuration process and push them to GitHub:');
log_1.default.log();
log_1.default.log('🔍 Checking for changes in the repository...');
await vcsClient.showChangedFilesAsync();
await (0, repository_1.reviewAndCommitChangesAsync)(vcsClient, `[eas-onboarding] Install dependencies${shouldSetupCredentials ? ' and set up build credentials' : ''}`, { nonInteractive: false });
log_1.default.log('📤 Pushing changes to GitHub...');
await (0, git_1.runGitPushAsync)({
targetProjectDir: finalTargetProjectDirectory,
});
}
else if (!app.githubRepository) {
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['add', '.'],
});
log_1.default.log();
await (0, runCommand_1.runCommandAsync)({
cwd: finalTargetProjectDirectory,
command: 'git',
args: ['commit', '-m', 'Initial commit'],
});
log_1.default.log();
}
log_1.default.log();
log_1.default.log('🎉 We finished configuring your project.');
log_1.default.log();
if (!!app.githubRepository ||
actor.preferences.onboarding.environment === generated_1.OnboardingEnvironment.ExpoGo) {
log_1.default.log('🚀 You can now go back to the website to continue:');
const url = new URL(`/onboarding/develop/set-up-project-on-your-machine?project=${app.slug}&accountId=${app.ownerAccount.id}`, (0, api_1.getExpoWebsiteBaseUrl)()).toString();
log_1.default.log(`👉 ${(0, log_1.link)(url)}`);
}
else {
log_1.default.log('🚀 Now we are going to trigger your first build');
log_1.default.log();
const { buildIds } = await (0, runBuildAndSubmit_1.runBuildAndSubmitAsync)(graphqlClient, analytics, vcsClient, finalTargetProjectDirectory, {
nonInteractive: true,
requestedPlatform: platform === eas_build_job_1.Platform.ANDROID ? platform_1.RequestedPlatform.Android : platform_1.RequestedPlatform.Ios,
profile: actor.preferences.onboarding.deviceType === generated_1.OnboardingDeviceType.Simulator
? 'development-simulator'
: 'development',
wait: false,
clearCache: false,
json: false,
autoSubmit: false,
localBuildOptions: {},
freezeCredentials: false,
repack: true,
}, actor, getDynamicProjectConfigFn);
const buildId = buildIds[0];
log_1.default.log();
log_1.default.log('🚀 You can now go back to the website to continue:');
const url = new URL(`/onboarding/develop/set-up-project-on-your-machine?project=${app.slug}&accountId=${app.ownerAccount.id}&buildId=${buildId}`, (0, api_1.getExpoWebsiteBaseUrl)()).toString();
log_1.default.log(`👉 ${(0, log_1.link)(url)}`);
}
const { __typename, ...previousPreferences } = actor.preferences.onboarding;
await UserPreferencesMutation_1.UserPreferencesMutation.markCliDoneInOnboardingUserPreferencesAsync(graphqlClient, {
...previousPreferences,
appId: app.id,
});
}
}
exports.default = Onboarding;
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
async function getPrivateExpoConfigWithProjectIdAsync({ projectDir, graphqlClient, actor, options, }) {
const expBefore = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, options);
const projectId = await (0, getProjectIdAsync_1.validateOrSetProjectIdAsync)({
exp: expBefore,
graphqlClient,
actor,
options: {
nonInteractive: false,
},
cwd: projectDir,
});
const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, options);
return {
exp,
projectId,
};
}
// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
function getDynamicPrivateProjectConfigGetter({ projectDir, graphqlClient, actor, }) {
return async (options) => {
return {
...(await getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
options,
})),
projectDir,
};
};
}
async function configureProjectFromBareDefaultExpoTemplateAsync({ app, vcsClient, targetDir, }) {
// Android package name requires each component to start with a lowercase letter.
const isUsernameValidSegment = /^[^a-z]/.test(app.ownerAccount.name);
const userPrefix = isUsernameValidSegment ? 'user' : '';
const isSlugValidSegment = /^[^a-z]/.test(app.slug);
const slugPrefix = isSlugValidSegment ? 'app' : '';
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(app.ownerAccount.name)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
const updateUrl = (0, api_1.getEASUpdateURL)(app.id);
const easBuildGitHubConfig = {
android: {
image: 'latest',
},
ios: {
image: 'latest',
},
};
const easJson = {
cli: {
version: `>= ${easCli_1.easCliVersion}`,
appVersionSource: eas_json_1.AppVersionSource.REMOTE,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
...easBuildGitHubConfig,
},
'development-simulator': {
extends: 'development',
ios: {
simulator: true,
},
},
preview: {
distribution: 'internal',
channel: 'main',
...easBuildGitHubConfig,
},
production: {
channel: 'production',
autoIncrement: true,
...easBuildGitHubConfig,
},
},
submit: {
production: {},
},
};
const easJsonPath = path_1.default.join(targetDir, 'eas.json');
await fs_extra_1.default.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcsClient.trackFileAsync(easJsonPath);
log_1.default.log(`✅ Generated ${chalk_1.default.bold('eas.json')}. ${(0, log_1.learnMore)('https://docs.expo.dev/build-reference/eas-json/')}`);
log_1.default.log();
const baseExpoConfig = JSON.parse(await fs_extra_1.default.readFile(path_1.default.join(targetDir, 'app.json'), 'utf8'))
.expo;
const expoConfig = {
...baseExpoConfig,
name: app.name ?? app.slug,
slug: app.slug,
extra: {
eas: {
projectId: app.id,
},
},
owner: app.ownerAccount.name,
updates: {
url: updateUrl,
},
runtimeVersion: {
policy: 'appVersion',
},
ios: {
...baseExpoConfig.ios,
bundleIdentifier,
},
android: {
...baseExpoConfig.android,
package: bundleIdentifier,
},
};
const appJsonPath = path_1.default.join(targetDir, 'app.json');
await fs_extra_1.default.writeFile(appJsonPath, `${JSON.stringify({ expo: expoConfig }, null, 2)}\n`);
await vcsClient.trackFileAsync(appJsonPath);
log_1.default.log(`✅ Generated ${chalk_1.default.bold('app.json')}. ${(0, log_1.learnMore)('https://docs.expo.dev/versions/latest/config/app/')}`);
log_1.default.log();
}
function stripInvalidCharactersForBundleIdentifier(string) {
return string.replaceAll(/[^A-Za-z0-9]/g, '');
}
;