Skip to content

Commit

Permalink
Merge branch 'main' into @p-malecki/manage-devices-enhancement
Browse files Browse the repository at this point in the history
  • Loading branch information
p-malecki committed Oct 10, 2024
2 parents 5220ef7 + 415e606 commit 1c24ff3
Show file tree
Hide file tree
Showing 25 changed files with 537 additions and 253 deletions.
49 changes: 40 additions & 9 deletions packages/docs/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,43 @@ Unfortunately debugging isn't available for the frontend code, however you can u

## Shared app template

In `test-apps/` directory there is `shared/` directory with the common UI code for
apps. To use this code in the app, use `copy.sh` script. It should use minimal
set of dependencies, ideally nothing except react-native.
If app uses expo-router, we additionally copy `shared/navigation/` directory
which uses expo-router and `@expo/vector-icons`.

If app wants to use this shared code, it should add an entry to .gitignore for
shared directory under its own `src/` and run copy script. If shared code is
updated, every app using it should rerun the copy script.
We provide few shared components with common code across tests apps in `shared/`
directory.
They only depend on `react-native`. Components in `shared/navigation` additionally
depend on `expo-router` and `expo-icons`.

To use them in the app:
1. Add npm command in test app package.json
- for expo-router apps: `"copy-shared": "../shared/copy.sh expo-router ./shared"`.
- for RN apps: `"copy-shared": "../shared/copy.sh bare ./shared"`.
2. Run it: `npm run copy-shared`. This copies shared components to `./shared`.
3. For RN apps, replace `App.tsx` with the `./shared/MainScreen.tsx` component.
```ts
import {MainScreen} from './shared/MainScreen';

export default MainScreen;
```
4. For apps with expo router, replace `app/(tabs)/_layout.tsx` and
`app/(tabs)/index.tsx` files.
```ts
// contents of `app/(tabs)/_layout.ts`
import { TabLayout } from "@/shared/navigation/TabLayout";
export default TabLayout;
```

```ts
// contents of `app/(tabs)/index.ts`
import { MainScreen } from "@/shared/MainScreen";
export default MainScreen;
```

You can also use other components in `shared` (e.g. `Text`, `Button`,
`useScheme`) to theme the app.

After updating shared components you need to copy them again by running
`npm run copy-shared` in every test app.

`shared/copy.sh bare|expo-router DEST` script works by copying shared directory to `DEST`
and removing `navigation` directory if `bare` argument is used.
9 changes: 0 additions & 9 deletions packages/vscode-extension/lib/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ const RNInternals = {
} catch (e) {}
throw new Error("Couldn't locate LoadingView module");
},
get DevMenu() {
return require("react-native/Libraries/NativeModules/specs/NativeDevMenu").default;
},
};

function getCurrentScene() {
Expand Down Expand Up @@ -246,12 +243,6 @@ export function PreviewAppWrapper({ children, initialProps, ..._rest }) {
[mainContainerRef]
);

useAgentListener(devtoolsAgent, "RNIDE_iosDevMenu", (_payload) => {
// this native module is present only on iOS and will crash if called
// on Android
RNInternals.DevMenu.show();
});

useAgentListener(
devtoolsAgent,
"RNIDE_showStorybookStory",
Expand Down
4 changes: 3 additions & 1 deletion packages/vscode-extension/src/builders/BuildManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export class BuildManager {
forceCleanBuild,
cancelToken,
this.buildOutputChannel,
progressListener
progressListener,
this.dependencyManager
);
} else {
this.buildOutputChannel = window.createOutputChannel("Radon IDE (iOS build)", {
Expand All @@ -93,6 +94,7 @@ export class BuildManager {
cancelToken,
this.buildOutputChannel,
progressListener,
this.dependencyManager,
installPodsIfNeeded
);
}
Expand Down
10 changes: 9 additions & 1 deletion packages/vscode-extension/src/builders/buildAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DevicePlatform } from "../common/DeviceManager";
import { getReactNativeVersion } from "../utilities/reactNative";
import { runExternalBuild } from "./customBuild";
import { fetchEasBuild } from "./eas";
import { DependencyManager } from "../dependency/DependencyManager";

export type AndroidBuildResult = {
platform: DevicePlatform.Android;
Expand Down Expand Up @@ -76,7 +77,8 @@ export async function buildAndroid(
forceCleanBuild: boolean,
cancelToken: CancelToken,
outputChannel: OutputChannel,
progressListener: (newProgress: number) => void
progressListener: (newProgress: number) => void,
dependencyManager: DependencyManager
): Promise<AndroidBuildResult> {
const { customBuild, eas, env, android } = getLaunchConfiguration();

Expand Down Expand Up @@ -117,6 +119,12 @@ export async function buildAndroid(
return { apkPath, packageName: EXPO_GO_PACKAGE_NAME, platform: DevicePlatform.Android };
}

if (!(await dependencyManager.isInstalled("android"))) {
throw new Error(
"Android directory does not exist, configure build source in launch configuration or use expo prebuild to generate the directory"
);
}

const androidSourceDir = getAndroidSourceDir(appRootFolder);
const productFlavor = android?.productFlavor || "";
const buildType = android?.buildType || "debug";
Expand Down
8 changes: 8 additions & 0 deletions packages/vscode-extension/src/builders/buildIOS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { findXcodeProject, findXcodeScheme, IOSProjectInfo } from "../utilities/
import { runExternalBuild } from "./customBuild";
import { fetchEasBuild } from "./eas";
import { getXcodebuildArch } from "../utilities/common";
import { DependencyManager } from "../dependency/DependencyManager";

export type IOSBuildResult = {
platform: DevicePlatform.IOS;
Expand Down Expand Up @@ -76,6 +77,7 @@ export async function buildIos(
cancelToken: CancelToken,
outputChannel: OutputChannel,
progressListener: (newProgress: number) => void,
dependencyManager: DependencyManager,
installPodsIfNeeded: () => Promise<void>
): Promise<IOSBuildResult> {
const { customBuild, eas, ios: buildOptions, env } = getLaunchConfiguration();
Expand Down Expand Up @@ -119,6 +121,12 @@ export async function buildIos(
return { appPath, bundleID: EXPO_GO_BUNDLE_ID, platform: DevicePlatform.IOS };
}

if (!(await dependencyManager.isInstalled("ios"))) {
throw new Error(
"Ios directory does not exist, configure build source in launch configuration or use expo prebuild to generate the directory"
);
}

const sourceDir = getIosSourceDir(appRootFolder);

await installPodsIfNeeded();
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/common/DependencyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type Dependency =
| "cocoaPods"
| "nodejs"
| "nodeModules"
| "android"
| "ios"
| "pods"
| "reactNative"
| "expo"
Expand Down
24 changes: 10 additions & 14 deletions packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ export type ReloadAction =
| "reboot" // reboots device, launch app
| "reinstall" // force reinstall app
| "restartProcess" // relaunch app
| "reloadJs" // refetch JS scripts from metro
| "hotReload";
| "reloadJs"; // refetch JS scripts from metro

export type Frame = {
x: number;
y: number;
width: number;
height: number;
};

export type InspectDataStackItem = {
componentName: string;
Expand All @@ -76,12 +82,7 @@ export type InspectDataStackItem = {
line0Based: number;
column0Based: number;
};
frame: {
x: number;
y: number;
width: number;
height: number;
};
frame: Frame;
};

export type TouchPoint = {
Expand All @@ -91,12 +92,7 @@ export type TouchPoint = {

export type InspectData = {
stack: InspectDataStackItem[] | undefined;
frame: {
x: number;
y: number;
width: number;
height: number;
};
frame: Frame;
};

export interface ProjectEventMap {
Expand Down
9 changes: 6 additions & 3 deletions packages/vscode-extension/src/debugging/DebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,15 @@ export class DebugAdapter extends DebugSession {
this.connection = new WebSocket(configuration.websocketAddress);

this.connection.on("open", () => {
this.sendCDPMessage("FuseboxClient.setClientMetadata", {}).catch(); // ignore error as it will throw on old debugger and is not critical
// the below catch handler is used to ignore errors coming from non critical CDP messages we
// expect in some setups to fail
const ignoreError = () => {};
this.sendCDPMessage("FuseboxClient.setClientMetadata", {}).catch(ignoreError);
this.sendCDPMessage("Runtime.enable", {});
this.sendCDPMessage("Debugger.enable", { maxScriptsCacheSize: 100000000 });
this.sendCDPMessage("Debugger.setPauseOnExceptions", { state: "none" });
this.sendCDPMessage("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }).catch(); // ignore error as it will throw on old debugger and is not critical
this.sendCDPMessage("Debugger.setBlackboxPatterns", { patterns: [] }).catch(); // ignore error as it will throw on old debugger and is not critical
this.sendCDPMessage("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }).catch(ignoreError);
this.sendCDPMessage("Debugger.setBlackboxPatterns", { patterns: [] }).catch(ignoreError);
this.sendCDPMessage("Runtime.runIfWaitingForDebugger", {});
this.sendCDPMessage("Runtime.evaluate", {
expression: "__RNIDE_onDebuggerConnected()",
Expand Down
35 changes: 34 additions & 1 deletion packages/vscode-extension/src/dependency/DependencyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "../common/DependencyManager";
import { shouldUseExpoCLI } from "../utilities/expoCli";
import { CancelToken } from "../builders/cancelToken";
import { getAndroidSourceDir } from "../builders/buildAndroid";

const STALE_PODS = "stalePods";

Expand Down Expand Up @@ -67,11 +68,19 @@ export class DependencyManager implements Disposable, DependencyManagerInterface
case "nodeModules":
return { status: await this.nodeModulesStatus(), isOptional: false };
case "pods":
return { status: await this.podsStatus(), isOptional: !isExpoGoProject() };
return { status: await this.podsStatus(), isOptional: await isExpoGoProject() };
case "reactNative": {
const status = dependencyStatus("react-native", MinSupportedVersion.reactNative);
return { status, isOptional: false };
}
case "android": {
const status = this.androidDirectoryExits() ? "installed" : "notInstalled";
return { status, isOptional: await areNativeDirectoriesOptional() };
}
case "ios": {
const status = this.iosDirectoryExits() ? "installed" : "notInstalled";
return { status, isOptional: await areNativeDirectoriesOptional() };
}
case "expo": {
const status = dependencyStatus("expo", MinSupportedVersion.expo);
return { status, isOptional: !shouldUseExpoCLI() };
Expand All @@ -98,6 +107,24 @@ export class DependencyManager implements Disposable, DependencyManagerInterface
return diagnostics;
}

androidDirectoryExits() {
const appRootFolder = getAppRootFolder();
const androidDirPath = getAndroidSourceDir(appRootFolder);
if (fs.existsSync(androidDirPath)) {
return true;
}
return false;
}

iosDirectoryExits() {
const appRootFolder = getAppRootFolder();
const iosDirPath = getIosSourceDir(appRootFolder);
if (fs.existsSync(iosDirPath)) {
return true;
}
return false;
}

public async isInstalled(dependency: Dependency) {
const status = await this.getStatus([dependency]);
return status[dependency].status === "installed";
Expand Down Expand Up @@ -321,3 +348,9 @@ function isUsingExpoRouter() {
return false;
}
}
async function areNativeDirectoriesOptional(): Promise<boolean> {
const isExpoGo = await isExpoGoProject();
const launchConfiguration = getLaunchConfiguration();

return isExpoGo && !!launchConfiguration.eas && !!launchConfiguration.customBuild;
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,6 @@ export class AndroidEmulatorDevice extends DeviceBase {
await this.changeSettings(settings);
}

async openDevMenu() {
await exec(ADB_PATH, ["-s", this.serial!, "shell", "input", "keyevent", "82"]);
}

async configureExpoDevMenu(packageName: string) {
if (packageName === "host.exp.exponent") {
// For expo go we are unable to change this setting as the APK is not debuggable
Expand Down
14 changes: 2 additions & 12 deletions packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class DeviceSession implements Disposable {
case "restartProcess":
await this.launchApp();
return true;
case "hotReload":
case "reloadJs":
if (this.devtools.hasConnectedClient) {
await this.metro.reload();
return true;
Expand Down Expand Up @@ -269,17 +269,7 @@ export class DeviceSession implements Disposable {
}

public async openDevMenu() {
// on iOS, we can load native module and dispatch dev menu show method. On
// Android, this native module isn't available and we need to fallback to
// adb to send "menu key" (code 82) to trigger code path showing the menu.
//
// We could probably unify it in the future by running metro in interactive
// mode and sending keys to stdin.
if (this.device.platform === DevicePlatform.IOS) {
this.devtools.send("RNIDE_iosDevMenu");
} else {
await (this.device as AndroidEmulatorDevice).openDevMenu();
}
await this.metro.openDevMenu();
}

public startPreview(previewId: string) {
Expand Down
17 changes: 17 additions & 0 deletions packages/vscode-extension/src/project/metro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { extensionContext, getAppRootFolder } from "../utilities/extensionContex
import { shouldUseExpoCLI } from "../utilities/expoCli";
import { Devtools } from "./devtools";
import { getLaunchConfiguration } from "../utilities/launchConfiguration";
import WebSocket from "ws";

Check warning on line 11 in packages/vscode-extension/src/project/metro.ts

View workflow job for this annotation

GitHub Actions / check

`ws` import should occur before import of `../utilities/subprocess`

export interface MetroDelegate {
onBundleError(): void;
Expand Down Expand Up @@ -234,6 +235,22 @@ export class Metro implements Disposable {
await appReady;
}

public async openDevMenu() {
// to send request to open dev menu, we route it through metro process
// that maintains a websocket connection with the device. Specifically,
// /message endpoint is used to send messages to the device, and metro proxies
// messages between different clients connected to that endpoint.
// Therefore, to send the message to the device we:
// 1. connect to the /message endpoint over websocket
// 2. send specifically formatted message to open dev menu
const ws = new WebSocket(`ws://localhost:${this._port}/message`);
await new Promise((resolve) => ws.addEventListener("open", resolve));
ws.send(
JSON.stringify({ version: 2 /* protocol version, needs to be set to 2 */, method: "devMenu" })
);
ws.close();
}

public async getDebuggerURL() {
const WAIT_FOR_DEBUGGER_TIMEOUT_MS = 15_000;

Expand Down
10 changes: 8 additions & 2 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,11 @@ export class Project
}

private async reloadMetro() {
if (await this.deviceSession?.perform("hotReload")) {
if (await this.deviceSession?.perform("reloadJs")) {
this.updateProjectState({ status: "running" });
return true;
}
return false;
}

public async goHome(homeUrl: string) {
Expand Down Expand Up @@ -302,7 +304,11 @@ export class Project
}

if (onlyReloadJSWhenPossible) {
return await this.reloadMetro();
// if reloading JS is possible, we try to do it first and exit in case of success
// otherwise we continue to restart using more invasive methods
if (await this.reloadMetro()) {
return;
}
}

// otherwise we try to restart the device session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@
.icons-container {
display: flex;
align-items: center;
}
}
Loading

0 comments on commit 1c24ff3

Please sign in to comment.