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

Commit

Permalink
[xdl] Allow dev client apps to be launched in the iOS simulator (#3182)
Browse files Browse the repository at this point in the history
  • Loading branch information
fson authored Feb 12, 2021
1 parent a5e81c4 commit 32bbf58
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 24 deletions.
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 @@ -242,6 +243,7 @@ export const startAsync = async (projectRoot: string, options: StartOptions) =>
await Simulator.openProjectAsync({
projectRoot,
shouldPrompt: true,
devClient: options.devClient ?? false,
});
printHelp();
break;
Expand All @@ -261,6 +263,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';
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

0 comments on commit 32bbf58

Please sign in to comment.