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

[xdl] Allow dev client apps to be launched in the iOS simulator #3182

Merged
merged 4 commits into from
Feb 12, 2021
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
35 changes: 15 additions & 20 deletions packages/config/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,29 +592,24 @@ function isDynamicFilePath(filePath: string): boolean {
}

/**
* Returns a string describing the configurations used for the given project root.
* Will return null if no config is found.
*
* @param projectRoot
* @param projectConfig
* Return a useful name describing the project config.
* - dynamic: app.config.js
* - static: app.json
* - custom path app config relative to root folder
* - both: app.config.js or app.json
*/
export function getProjectConfigDescription(
projectRoot: string,
projectConfig: Partial<ProjectConfig>
): string | null {
if (projectConfig.dynamicConfigPath) {
const relativeDynamicConfigPath = path.relative(projectRoot, projectConfig.dynamicConfigPath);
if (projectConfig.staticConfigPath) {
return `Using dynamic config \`${relativeDynamicConfigPath}\` and static config \`${path.relative(
projectRoot,
projectConfig.staticConfigPath
)}\``;
export function getProjectConfigDescription(projectDir: string): string {
const paths = getConfigFilePaths(projectDir);
if (paths.dynamicConfigPath) {
const relativeDynamicConfigPath = path.relative(projectDir, paths.dynamicConfigPath);
if (paths.staticConfigPath) {
return `${relativeDynamicConfigPath} or ${path.relative(projectDir, paths.staticConfigPath)}`;
}
return `Using dynamic config \`${relativeDynamicConfigPath}\``;
} else if (projectConfig.staticConfigPath) {
return `Using static config \`${path.relative(projectRoot, projectConfig.staticConfigPath)}\``;
return relativeDynamicConfigPath;
} else if (paths.staticConfigPath) {
return path.relative(projectDir, paths.staticConfigPath);
}
return null;
return 'app.config.js/app.json';
}

export * from './Config.types';
3 changes: 3 additions & 0 deletions packages/expo-cli/src/commands/start/TerminalUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const BLT = `\u203A`;
const { bold: b, italic: i, underline: u } = chalk;

type StartOptions = {
devClient?: boolean;
reset?: boolean;
nonInteractive?: boolean;
nonPersistent?: boolean;
Expand Down Expand Up @@ -208,6 +209,7 @@ export const startAsync = async (projectRoot: string, options: StartOptions) =>
await Simulator.openProjectAsync({
projectRoot,
shouldPrompt: true,
devClient: options.devClient ?? false,
});
printHelp();
break;
Expand All @@ -227,6 +229,7 @@ export const startAsync = async (projectRoot: string, options: StartOptions) =>
await Simulator.openProjectAsync({
projectRoot,
shouldPrompt: false,
devClient: options.devClient ?? false,
});
printHelp();
break;
Expand Down
144 changes: 144 additions & 0 deletions packages/xdl/src/BundleIdentifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* BundleIdentifier.ts
*
* NOTE:
* The code in this module originates from eas-cli and the canonical version of
* it is in
* https://github.com/expo/eas-cli/blob/6a0a9bbaaad03b053b5930f7d14bde35b4d0a9f0/packages/eas-cli/src/build/ios/bundleIdentifer.ts#L36
* Any changes to this code should be applied to eas-cli as well!
*
* TODO: move the code for configuring and validating the bundle identifier
* to a shared package that can be used for both eas-cli and expo-cli.
*/
import { ExpoConfig, getConfigFilePaths, getProjectConfigDescription } from '@expo/config';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add a big doc block here to point to the canonical version of this code in eas-cli, so it's clearly documented that this is a copy/paste fork of it

import { IOSConfig } from '@expo/config-plugins';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
import { prompt } from 'prompts';

import { logInfo, logWarning } from './project/ProjectUtils';

enum BundleIdentiferSource {
XcodeProject,
AppJson,
}

export async function configureBundleIdentifierAsync(
projectRoot: string,
exp: ExpoConfig
): Promise<string> {
const configDescription = getProjectConfigDescription(projectRoot);
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectRoot
);
const bundleIdentifierFromConfig = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
if (bundleIdentifierFromPbxproj === bundleIdentifierFromConfig) {
return bundleIdentifierFromConfig;
} else {
logWarning(
projectRoot,
'expo',
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
);
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
projectRoot,
exp
);
if (!hasBundleIdentifierInStaticConfig) {
logInfo(
projectRoot,
'expo',
`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`
);
}
const { bundleIdentifierSource } = await prompt({
type: 'select',
name: 'bundleIdentifierSource',
message: 'Which bundle identifier should we use?',
choices: [
{
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
value: BundleIdentiferSource.XcodeProject,
},
{
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
value: BundleIdentiferSource.AppJson,
},
],
});
if (bundleIdentifierSource === BundleIdentiferSource.XcodeProject) {
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectRoot,
bundleIdentifierFromConfig
);
return bundleIdentifierFromConfig;
} else {
if (hasBundleIdentifierInStaticConfig) {
await updateAppJsonConfigAsync(projectRoot, exp, bundleIdentifierFromPbxproj);
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
return bundleIdentifierFromPbxproj;
}
}
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
if (getConfigFilePaths(projectRoot).staticConfigPath) {
await updateAppJsonConfigAsync(projectRoot, exp, bundleIdentifierFromPbxproj);
return bundleIdentifierFromPbxproj;
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectRoot,
bundleIdentifierFromConfig
);
return bundleIdentifierFromConfig;
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
}

function missingBundleIdentifierMessage(configDescription: string): string {
return `Please define "ios.bundleIdentifier" in ${configDescription}.`;
}

async function updateAppJsonConfigAsync(
projectDir: string,
exp: ExpoConfig,
newBundleIdentifier: string
): Promise<void> {
const paths = getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "Can't update dynamic configs");

const rawStaticConfig = (await JsonFile.readAsync(paths.staticConfigPath)) as any;
rawStaticConfig.expo = {
...rawStaticConfig.expo,
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier: newBundleIdentifier },
};
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig);

exp.ios = { ...exp.ios, bundleIdentifier: newBundleIdentifier };
}

/**
* Check if static config exists and if ios.bundleIdentifier is defined there.
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
*/
async function hasBundleIdentifierInStaticConfigAsync(
projectDir: string,
exp: ExpoConfig
): Promise<boolean> {
if (!exp.ios?.bundleIdentifier) {
return false;
}
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
return false;
}
const rawStaticConfig = JsonFile.read(paths.staticConfigPath) as any;
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
}
39 changes: 35 additions & 4 deletions packages/xdl/src/Simulator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getConfig } from '@expo/config';
import { ExpoConfig, getConfig } from '@expo/config';
import * as osascript from '@expo/osascript';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
Expand All @@ -10,6 +10,7 @@ import semver from 'semver';

import Analytics from './Analytics';
import Api from './Api';
import { configureBundleIdentifierAsync } from './BundleIdentifier';
import Logger from './Logger';
import NotificationCode from './NotificationCode';
import * as Prompts from './Prompts';
Expand All @@ -19,6 +20,7 @@ import UserSettings from './UserSettings';
import * as Versions from './Versions';
import { getUrlAsync as getWebpackUrlAsync } from './Webpack';
import * as Xcode from './Xcode';
import { learnMore } from './logs/TerminalLink';
import { delayAsync } from './utils/delayAsync';

let _lastUrl: string | null = null;
Expand Down Expand Up @@ -533,16 +535,22 @@ export async function upgradeExpoAsync(
return true;
}

export async function openUrlInSimulatorSafeAsync({
async function openUrlInSimulatorSafeAsync({
url,
udid,
isDetached = false,
sdkVersion,
devClient = false,
projectRoot,
exp = getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp,
}: {
url: string;
udid?: string;
sdkVersion?: string;
isDetached: boolean;
devClient?: boolean;
exp?: ExpoConfig;
projectRoot: string;
}): Promise<{ success: true } | { success: false; msg: string }> {
if (!(await isSimulatorInstalledAsync())) {
return {
Expand All @@ -562,7 +570,10 @@ export async function openUrlInSimulatorSafeAsync({
}

try {
if (!isDetached) {
if (devClient) {
const bundleIdentifier = await configureBundleIdentifierAsync(projectRoot, exp);
await assertDevClientInstalledAsync(simulator, bundleIdentifier);
} else if (!isDetached) {
await ensureExpoClientInstalledAsync(simulator, sdkVersion);
_lastUrl = url;
}
Expand All @@ -585,7 +596,7 @@ export async function openUrlInSimulatorSafeAsync({
`Error running app. Have you installed the app already using Xcode? Since you are detached you must build manually. ${e.toString()}`
);
} else {
Logger.global.error(`Error installing or running app. ${e.toString()}`);
Logger.global.error(e.message);
}

return {
Expand All @@ -603,6 +614,20 @@ export async function openUrlInSimulatorSafeAsync({
};
}

async function assertDevClientInstalledAsync(
simulator: Pick<SimControl.SimulatorDevice, 'udid' | 'name'>,
bundleIdentifier: string
): Promise<void> {
if (!(await SimControl.getContainerPathAsync(simulator.udid, bundleIdentifier))) {
throw new Error(
`The development client (${bundleIdentifier}) for this project is not installed. ` +
`Please build and install the client on the simulator first.\n${learnMore(
'https://docs.expo.io/clients/distribution-for-ios/#building-for-ios'
)}`
);
}
}

// Keep a list of simulator UDIDs so we can prevent asking multiple times if a user wants to upgrade.
// This can prevent annoying interactions when they don't want to upgrade for whatever reason.
const hasPromptedToUpgrade: Record<string, boolean> = {};
Expand Down Expand Up @@ -655,9 +680,11 @@ async function getClientForSDK(sdkVersionString?: string) {
export async function openProjectAsync({
projectRoot,
shouldPrompt,
devClient,
}: {
projectRoot: string;
shouldPrompt?: boolean;
devClient?: boolean;
}): Promise<{ success: true; url: string } | { success: false; error: string }> {
const projectUrl = await UrlUtils.constructDeepLinkAsync(projectRoot, {
hostType: 'localhost',
Expand All @@ -683,6 +710,9 @@ export async function openProjectAsync({
url: projectUrl,
sdkVersion: exp.sdkVersion,
isDetached: !!exp.isDetached,
devClient,
exp,
projectRoot,
});

if (result.success) {
Expand Down Expand Up @@ -723,6 +753,7 @@ export async function openWebProjectAsync({
url: projectUrl,
udid: device.udid,
isDetached: true,
projectRoot,
});
if (result.success) {
await activateSimulatorWindowAsync();
Expand Down