Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
[expo-cli] Combined ID prompts for build and eject (#2313)
Browse files Browse the repository at this point in the history
* Combined ID prompts for build and eject

* Move prompt to after validation

* Update Eject.ts
  • Loading branch information
EvanBacon authored Jul 1, 2020
1 parent c4fed00 commit 7f0d102
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 150 deletions.
21 changes: 10 additions & 11 deletions packages/expo-cli/src/commands/build/AndroidBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import BuildError from './BuildError';
import BaseBuilder from './BaseBuilder';
import * as utils from './utils';
import { PLATFORMS, Platform } from './constants';
import { getOrPromptForPackage } from '../eject/ConfigValidation';

const { ANDROID } = PLATFORMS;

export default class AndroidBuilder extends BaseBuilder {
async run(): Promise<void> {
// This gets run after all other validation to prevent users from having to answer this question multiple times.
this.options.type = await utils.askBuildType(this.options.type!, {
apk: 'Build a package to deploy to the store or install directly on Android devices',
'app-bundle': 'Build an optimized bundle for the store',
});

// Check SplashScreen images sizes
await Android.checkSplashScreenImages(this.projectDir);

Expand All @@ -43,17 +50,9 @@ export default class AndroidBuilder extends BaseBuilder {
await utils.checkIfSdkIsSupported(this.manifest.sdkVersion!, ANDROID);

// Check the android package name
// TODO: Attempt to automatically write this value.
const androidPackage = this.manifest.android?.package;
if (!androidPackage) {
throw new BuildError(`Your project must have an Android package set in app.json
See https://docs.expo.io/distribution/building-standalone-apps/#2-configure-appjson`);
}
if (!/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(androidPackage)) {
throw new BuildError(
"Invalid format of Android package name (only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter)"
);
}
await getOrPromptForPackage(this.projectDir);

this.updateProjectConfig();
}

platform(): Platform {
Expand Down
5 changes: 1 addition & 4 deletions packages/expo-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ export default function (program: Command) {
);
process.exit(1);
}
options.type = await askBuildType(options.type, {
apk: 'Build a package to deploy to the store or install directly on Android devices',
'app-bundle': 'Build an optimized bundle for the store',
});

const androidBuilder = new AndroidBuilder(projectDir, options);
return androidBuilder.command();
},
Expand Down
76 changes: 4 additions & 72 deletions packages/expo-cli/src/commands/build/ios/IOSBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { XDLError } from '@expo/xdl';

import terminalLink from 'terminal-link';
import semver from 'semver';
import { modifyConfigAsync } from '@expo/config';
import BaseBuilder from '../BaseBuilder';
import { PLATFORMS } from '../constants';
import * as utils from '../utils';
Expand Down Expand Up @@ -37,6 +36,7 @@ import {
useProvisioningProfileFromParams,
} from '../../../credentials/views/IosProvisioningProfile';
import { IosAppCredentials, IosDistCredentials } from '../../../credentials/credentials';
import { getOrPromptForBundleIdentifier } from '../../eject/ConfigValidation';

const noBundleIdMessage = `Your project must have a \`bundleIdentifier\` set in the Expo config (app.json or app.config.js).\nSee https://expo.fyi/bundle-identifier`;

Expand Down Expand Up @@ -126,77 +126,9 @@ class IOSBuilder extends BaseBuilder {
await this.validateIcon();

// Check the bundle ID and possibly prompt the user to add a new one.
const bundleIdentifier = this.manifest.ios?.bundleIdentifier;

if (!bundleIdentifier) {
// Recommend a bundle ID based on the username and project slug.
const username = await this.getUsernameAsync();
const recommendedBundleId = username ? `com.${username}.${this.manifest.slug}` : undefined;

log.newLine();
log(
log.chalk.cyan(
`Now we need to know your ${terminalLink(
'iOS bundle identifier',
'https://expo.fyi/bundle-identifier'
)}.\nYou can change this in the future if you need to.`
)
);
log.newLine();

// Prompt the user for the bundle ID.
// Even if the project is using a dynamic config we can still
// prompt a better error message, recommend a default value, and help the user
// validate their custom bundle ID upfront.
const bundleIdPrompt = await prompt(
[
{
name: 'bundleIdentifier',
default: recommendedBundleId,
// The Apple helps people know this isn't an EAS feature.
message: `What would you like your iOS bundle identifier to be?`,
validate: (value: string) => /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(value),
},
],
{
nonInteractiveHelp: noBundleIdMessage,
}
);

const modification = await modifyConfigAsync(
this.projectDir,
{ ios: { bundleIdentifier: bundleIdPrompt.bundleIdentifier } },
{ skipSDKVersionRequirement: true }
);
if (modification.type === 'success') {
log.newLine();
// Success!
log(`Your iOS bundle identifier is now: ${bundleIdPrompt.bundleIdentifier}`);
log.newLine();
} else {
log.newLine();
if (modification.type === 'warn') {
// The project is using a dynamic config, give the user a helpful log and bail out.
log(log.chalk.yellow(modification.message));
} else {
log(
log.chalk.yellow(
'No Expo config was found. Please create an Expo config (`app.config.js` or `app.json`) in your project root.'
)
);
}

log(log.chalk.cyan(`Please add the following to your Expo config, and try again... `));
log.newLine();
log(
JSON.stringify({ ios: { bundleIdentifier: bundleIdPrompt.bundleIdentifier } }, null, 2)
);
log.newLine();
process.exit(1);
}
// Update with the latest bundle ID
this.updateProjectConfig();
}
await getOrPromptForBundleIdentifier(this.projectDir);
// Update with the latest bundle ID
this.updateProjectConfig();
}

private async getUsernameAsync(): Promise<string | undefined> {
Expand Down
194 changes: 194 additions & 0 deletions packages/expo-cli/src/commands/eject/ConfigValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
import { UserManager } from '@expo/xdl';
import terminalLink from 'terminal-link';

import log from '../../log';
import prompt from '../../prompt';

const noBundleIdMessage = `Your project must have a \`bundleIdentifier\` set in the Expo config (app.json or app.config.js).\nSee https://expo.fyi/bundle-identifier`;
const noPackageMessage = `Your project must have a \`package\` set in the Expo config (app.json or app.config.js).\nSee https://expo.fyi/android-package`;

function validateBundleId(value: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(value);
}

function validatePackage(value: string): boolean {
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(value);
}

export async function getOrPromptForBundleIdentifier(projectRoot: string): Promise<string> {
let { exp } = getConfig(projectRoot);

const currentBundleId = exp.ios?.bundleIdentifier;
if (currentBundleId) {
if (validateBundleId(currentBundleId)) {
return currentBundleId;
}

log(
log.chalk.red(
`The ios.bundleIdentifier defined in your Expo config is not formatted properly. Only alphanumeric characters, '.', '-', and '_' are allowed, and each '.' must be followed by a letter.`
)
);

process.exit(1);
}

// Recommend a bundle ID based on the username and project slug.
let recommendedBundleId: string | undefined;
// Attempt to use the android package name first since it's convenient to have them aligned.
if (exp.android?.package && validateBundleId(exp.android?.package)) {
recommendedBundleId = exp.android?.package;
} else {
const username = exp.owner ?? (await UserManager.getCurrentUsernameAsync());
const possibleId = `com.${username}.${exp.slug}`;
if (username && validateBundleId(possibleId)) {
recommendedBundleId = possibleId;
}
}

log.newLine();
log(
log.chalk.cyan(
`Now we need to know your ${terminalLink(
'iOS bundle identifier',
'https://expo.fyi/bundle-identifier'
)}.\nYou can change this in the future if you need to.`
)
);
log.newLine();

// Prompt the user for the bundle ID.
// Even if the project is using a dynamic config we can still
// prompt a better error message, recommend a default value, and help the user
// validate their custom bundle ID upfront.
const { bundleIdentifier } = await prompt(
[
{
name: 'bundleIdentifier',
default: recommendedBundleId,
// The Apple helps people know this isn't an EAS feature.
message: `What would you like your iOS bundle identifier to be?`,
validate: validateBundleId,
},
],
{
nonInteractiveHelp: noBundleIdMessage,
}
);

await attemptModification(projectRoot, `Your iOS bundle identifier is now: ${bundleIdentifier}`, {
ios: { bundleIdentifier },
});

return bundleIdentifier;
}

export async function getOrPromptForPackage(projectRoot: string): Promise<string> {
let { exp } = getConfig(projectRoot);

const currentPackage = exp.android?.package;
if (currentPackage) {
if (validatePackage(currentPackage)) {
return currentPackage;
}
log(
log.chalk.red(
`Invalid format of Android package name. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
)
);

process.exit(1);
}

// Recommend a package name based on the username and project slug.
let recommendedPackage: string | undefined;
// Attempt to use the ios bundle id first since it's convenient to have them aligned.
if (exp.ios?.bundleIdentifier && validatePackage(exp.ios.bundleIdentifier)) {
recommendedPackage = exp.ios.bundleIdentifier;
} else {
const username = exp.owner ?? (await UserManager.getCurrentUsernameAsync());
const possibleId = `com.${username}.${exp.slug}`;
if (username && validatePackage(possibleId)) {
recommendedPackage = possibleId;
}
}

log.newLine();
log(
`Now we need to know your ${terminalLink(
'Android package',
'https://expo.fyi/android-package'
)}. You can change this in the future if you need to.`
);
log.newLine();

// Prompt the user for the android package.
// Even if the project is using a dynamic config we can still
// prompt a better error message, recommend a default value, and help the user
// validate their custom android package upfront.
const { packageName } = await prompt(
[
{
name: 'packageName',
default: recommendedPackage,
message: `What would you like your Android package to be named?`,
validate: validatePackage,
},
],
{
nonInteractiveHelp: noPackageMessage,
}
);

await attemptModification(projectRoot, `Your Android package is now: ${packageName}`, {
android: { package: packageName },
});

return packageName;
}

async function attemptModification(
projectRoot: string,
modificationSuccessMessage: string,
edits: Partial<ExpoConfig>
): Promise<void> {
const modification = await modifyConfigAsync(projectRoot, edits, {
skipSDKVersionRequirement: true,
});
if (modification.type === 'success') {
log.newLine();
log(modificationSuccessMessage);
log.newLine();
} else {
warnAboutConfigAndExit(modification.type, modification.message!, edits);
}
}

function logNoConfig() {
log(
log.chalk.yellow(
'No Expo config was found. Please create an Expo config (`app.config.js` or `app.json`) in your project root.'
)
);
}

function warnAboutConfigAndExit(type: string, message: string, edits: Partial<ExpoConfig>) {
log.newLine();
if (type === 'warn') {
// The project is using a dynamic config, give the user a helpful log and bail out.
log(log.chalk.yellow(message));
} else {
logNoConfig();
}

notifyAboutManualConfigEdits(edits);
process.exit(1);
}

function notifyAboutManualConfigEdits(edits: Partial<ExpoConfig>) {
log(log.chalk.cyan(`Please add the following to your Expo config, and try again... `));
log.newLine();
log(JSON.stringify(edits, null, 2));
log.newLine();
}
Loading

0 comments on commit 7f0d102

Please sign in to comment.