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

[expo-cli] Combined ID prompts for build and eject #2313

Merged
merged 4 commits into from
Jul 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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