('');
4 | export const useWindowId = () => useContext(WindowContext);
5 |
6 | type WindowProviderProps = {
7 | children: React.ReactNode;
8 | id: string;
9 | };
10 |
11 | export function WindowProvider({ children, id }: WindowProviderProps) {
12 | return {children};
13 | }
14 |
15 | export const withWindowProvider = (
16 | WrappedComponent: React.ComponentType
,
17 | id: string
18 | ) => {
19 | const WithWindowProvider = (props: P) => {
20 | return (
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | return WithWindowProvider;
28 | };
29 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/modules/WindowManager/index.web.ts:
--------------------------------------------------------------------------------
1 | import { AppRegistry } from 'react-native';
2 | import { requireElectronModule } from 'react-native-electron-modules/build/requireElectronModule';
3 |
4 | import { withWindowProvider } from './WindowProvider';
5 | import { WindowsConfig, WindowsManagerType } from './types';
6 | import { withFluentProvider } from '../../providers/FluentProvider';
7 | import { withThemeProvider } from '../../utils/useExpoTheme';
8 |
9 | export { WindowStyleMask } from './types';
10 |
11 | export const WindowManager = requireElectronModule('WindowManager');
12 |
13 | export function createWindowsNavigator(config: T) {
14 | Object.entries(config).forEach(([key, value]) => {
15 | AppRegistry.registerComponent(key, () =>
16 | withWindowProvider(withFluentProvider(withThemeProvider(value.component)), key)
17 | );
18 | });
19 |
20 | return {
21 | open: (windowName: keyof T) => {
22 | WindowManager.openWindow(String(windowName), config[windowName].options || {});
23 | },
24 | close: (window: keyof T) => {
25 | WindowManager.closeWindow(String(window));
26 | },
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/modules/WindowManager/types.ts:
--------------------------------------------------------------------------------
1 | export enum WindowStyleMask {
2 | Borderless,
3 | Titled,
4 | Closable,
5 | Miniaturizable,
6 | Resizable,
7 | UnifiedTitleAndToolbar,
8 | FullScreen,
9 | FullSizeContentView,
10 | UtilityWindow,
11 | DocModalWindow,
12 | NonactivatingPanel,
13 | }
14 |
15 | export type WindowOptions = {
16 | title?: string;
17 | windowStyle?: {
18 | mask?: WindowStyleMask[];
19 | height?: number;
20 | width?: number;
21 | titlebarAppearsTransparent?: boolean;
22 | };
23 | };
24 |
25 | export type WindowsConfig = {
26 | [key: string]: {
27 | component: React.ComponentType;
28 | options?: WindowOptions;
29 | };
30 | };
31 |
32 | export type WindowsManagerType = {
33 | openWindow: (window: string, options: WindowOptions) => Promise;
34 | closeWindow(window: string): void;
35 | };
36 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/modules/WindowManager/useWindowFocus.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import { useWindowId } from './WindowProvider';
4 | import { DeviceEventEmitter } from '../DeviceEventEmitter';
5 |
6 | export function useWindowFocusEffect(callback: () => void) {
7 | const windowId = useWindowId();
8 |
9 | useEffect(() => {
10 | const listener = DeviceEventEmitter.addListener('windowFocused', (focusedWindowId: string) => {
11 | if (focusedWindowId === windowId) {
12 | callback();
13 | }
14 | });
15 |
16 | return () => {
17 | listener.remove();
18 | };
19 | }, [callback, windowId]);
20 | }
21 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/DeviceListSectionHeader.tsx:
--------------------------------------------------------------------------------
1 | import { TouchableOpacity } from 'react-native';
2 |
3 | import SectionHeader from './SectionHeader';
4 | import AlertIcon from '../assets/icons/AlertTriangle';
5 | import { View } from '../components/View';
6 | import Alert from '../modules/Alert';
7 | import { PlatformColor } from '../modules/PlatformColor';
8 | import { useCurrentTheme } from '../utils/useExpoTheme';
9 |
10 | type Props = {
11 | label: string;
12 | errorMessage?: string;
13 | };
14 |
15 | const DeviceListSectionHeader = ({ label, errorMessage }: Props) => {
16 | const theme = useCurrentTheme();
17 |
18 | return (
19 |
20 | Alert.alert('Something went wrong', errorMessage)}>
27 |
33 |
34 | ) : null
35 | }
36 | />
37 |
38 | );
39 | };
40 |
41 | export default DeviceListSectionHeader;
42 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/DevicesListError.tsx:
--------------------------------------------------------------------------------
1 | import { TouchableOpacity } from 'react-native';
2 |
3 | import AlertIcon from '../assets/icons/AlertTriangle';
4 | import { View, Text } from '../components';
5 | import Alert from '../modules/Alert';
6 |
7 | const DevicesListError = ({ error }: { error: Error }) => {
8 | return (
9 |
10 | Alert.alert('Something went wrong', error.message)}>
13 |
14 | Something went wrong
15 |
16 | Unable to list devices, click here to see the full error
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default DevicesListError;
24 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ErrorInfo, FunctionComponent, PropsWithChildren, createElement } from 'react';
2 |
3 | export type FallbackProps = {
4 | error?: Error;
5 | errorInfo?: string;
6 | };
7 |
8 | type Props = PropsWithChildren<{
9 | fallback: FunctionComponent;
10 | }>;
11 | type State = {
12 | hasError: boolean;
13 | error?: Error;
14 | errorInfo?: string;
15 | };
16 |
17 | export class ErrorBoundary extends React.Component {
18 | constructor(props: Props) {
19 | super(props);
20 | this.state = {
21 | hasError: false,
22 | };
23 | }
24 |
25 | static getDerivedStateFromError(_error: Error) {
26 | return { hasError: true };
27 | }
28 |
29 | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
30 | this.setState({
31 | error,
32 | errorInfo: JSON.stringify(errorInfo),
33 | });
34 | }
35 |
36 | render() {
37 | if (this.state.hasError) {
38 | // You can render any custom fallback UI
39 | return createElement(this.props.fallback, {
40 | error: this.state.error,
41 | errorInfo: this.state.errorInfo,
42 | });
43 | }
44 |
45 | return this.props.children;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import Item from './Item';
5 | import { Divider, Text, View } from '../components';
6 | import MenuBarModule from '../modules/MenuBarModule';
7 | import { WindowsNavigator } from '../windows';
8 |
9 | export const FOOTER_HEIGHT = 62;
10 |
11 | const Footer = () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | - WindowsNavigator.open('Settings')}>
19 | Settings…
20 |
21 | -
22 | Quit
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default memo(Footer);
30 |
31 | const styles = StyleSheet.create({
32 | container: {
33 | height: FOOTER_HEIGHT,
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/Item.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, PropsWithChildren, useState } from 'react';
2 | import { Pressable, PressableProps, StyleProp, StyleSheet, ViewStyle } from 'react-native';
3 |
4 | import { Row, Text } from '../components';
5 | import { useCurrentTheme } from '../utils/useExpoTheme';
6 |
7 | type Props = PropsWithChildren & {
8 | shortcut?: string;
9 | style?: StyleProp;
10 | };
11 |
12 | const Item = ({ children, onPress, shortcut, style }: Props) => {
13 | const [isHovered, setHovered] = useState(false);
14 | const theme = useCurrentTheme();
15 |
16 | return (
17 | setHovered(true)}
19 | onHoverOut={() => setHovered(false)}
20 | onPress={onPress}
21 | style={[
22 | styles.itemContainer,
23 | style,
24 | isHovered && {
25 | backgroundColor: theme === 'dark' ? 'rgba(255,255,255,.12)' : 'rgba(0,0,0,.12)',
26 | },
27 | ]}>
28 |
29 | {children}
30 | {shortcut && {shortcut}}
31 |
32 |
33 | );
34 | };
35 |
36 | export default memo(Item);
37 |
38 | const styles = StyleSheet.create({
39 | itemContainer: {
40 | borderRadius: 4,
41 | marginHorizontal: 6,
42 | },
43 | shortcut: {
44 | marginLeft: 'auto',
45 | opacity: 0.4,
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/popover/SectionHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { Row, Text } from '../components';
5 | import { useTheme } from '../providers/ThemeProvider';
6 |
7 | export const SECTION_HEADER_HEIGHT = 20;
8 |
9 | type Props = {
10 | label: string;
11 | accessoryRight?: React.ReactNode;
12 | };
13 |
14 | const SectionHeader = ({ accessoryRight, label }: Props) => {
15 | const theme = useTheme();
16 | return (
17 |
18 |
23 | {label}
24 |
25 | {accessoryRight ? accessoryRight : null}
26 |
27 | );
28 | };
29 |
30 | export default memo(SectionHeader);
31 |
32 | const styles = StyleSheet.create({
33 | row: { height: SECTION_HEADER_HEIGHT },
34 | });
35 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/providers/FluentProvider/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 |
3 | export const withFluentProvider = (WrappedComponent: React.ComponentType
) => {
4 | return (props: P) => {
5 | return ;
6 | };
7 | };
8 |
9 | export const FluentProvider = ({ children }: { children: ReactElement }) => {
10 | return children;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/providers/FluentProvider/index.web.tsx:
--------------------------------------------------------------------------------
1 | import { FluentProvider as ReactFluentProvider } from '@fluentui/react-provider';
2 | import { webLightTheme, webDarkTheme, Theme } from '@fluentui/react-theme';
3 | import { CSSProperties, ComponentType, ReactElement } from 'react';
4 | import { useColorScheme } from 'react-native';
5 |
6 | const lightTheme: Theme = {
7 | ...webLightTheme,
8 | colorNeutralBackground1: 'var(--orbit-window-background)',
9 | };
10 |
11 | const darkTheme: Theme = {
12 | ...webDarkTheme,
13 | colorNeutralBackground1: 'var(--orbit-window-background)',
14 | };
15 |
16 | export const FluentProvider = ({
17 | children,
18 | style,
19 | }: {
20 | children: ReactElement;
21 | style?: CSSProperties;
22 | }) => {
23 | const scheme = useColorScheme();
24 | const theme = scheme === 'dark' ? darkTheme : lightTheme;
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export const withFluentProvider =
(WrappedComponent: ComponentType
) => {
34 | const WithFluentProvider = (props: P) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | return WithFluentProvider;
43 | };
44 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useColorScheme } from 'react-native';
3 |
4 | type ThemePreference = 'light' | 'dark' | 'no-preference';
5 | type Theme = 'light' | 'dark';
6 |
7 | const ThemeContext = React.createContext('light');
8 | export const useTheme = () => React.useContext(ThemeContext);
9 |
10 | type ThemeProviderProps = {
11 | children: React.ReactNode;
12 | themePreference?: ThemePreference;
13 | };
14 |
15 | export function ThemeProvider({ children, themePreference = 'no-preference' }: ThemeProviderProps) {
16 | const systemTheme = useColorScheme();
17 |
18 | const theme = React.useMemo(() => {
19 | if (themePreference !== 'no-preference') {
20 | return themePreference;
21 | }
22 |
23 | return systemTheme ?? 'light';
24 | }, [themePreference, systemTheme]);
25 |
26 | return {children};
27 | }
28 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { Linking } from '../modules/Linking';
2 | import MenuBarModule from '../modules/MenuBarModule';
3 |
4 | export const openProjectsSelectorURL = () => {
5 | Linking.openURL('https://expo.dev/accounts/[account]/projects/[project]/builds');
6 | MenuBarModule.closePopover();
7 | };
8 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/utils/useExpoTheme.tsx:
--------------------------------------------------------------------------------
1 | import { lightTheme, darkTheme, palette } from '@expo/styleguide-native';
2 | import * as React from 'react';
3 |
4 | import { ThemeProvider, useTheme } from '../providers/ThemeProvider';
5 |
6 | export type ExpoTheme = typeof lightTheme;
7 |
8 | export function useCurrentTheme(): 'light' | 'dark' {
9 | const theme = useTheme();
10 | return theme;
11 | }
12 |
13 | export function useExpoTheme(): ExpoTheme {
14 | const theme = useTheme();
15 |
16 | if (theme === 'dark') {
17 | return darkTheme;
18 | }
19 |
20 | return lightTheme;
21 | }
22 |
23 | export function useExpoPalette() {
24 | const theme = useTheme();
25 | return palette[theme];
26 | }
27 |
28 | export const withThemeProvider = (WrappedComponent: React.ComponentType
) => {
29 | const WithThemeProvider = (props: P) => {
30 | return (
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | return WithThemeProvider;
38 | };
39 |
--------------------------------------------------------------------------------
/apps/menu-bar/src/windows/index.ts:
--------------------------------------------------------------------------------
1 | import DebugMenu from './DebugMenu';
2 | import Onboarding from './Onboarding';
3 | import Settings from './Settings';
4 | import { WindowStyleMask, createWindowsNavigator } from '../modules/WindowManager';
5 |
6 | export const WindowsNavigator = createWindowsNavigator({
7 | Settings: {
8 | component: Settings,
9 | options: {
10 | title: 'Settings',
11 | windowStyle: {
12 | mask: [WindowStyleMask.Titled, WindowStyleMask.Closable],
13 | titlebarAppearsTransparent: true,
14 | height: 580,
15 | width: 500,
16 | },
17 | },
18 | },
19 | Onboarding: {
20 | component: Onboarding,
21 | options: {
22 | title: '',
23 | windowStyle: {
24 | mask: [WindowStyleMask.Titled, WindowStyleMask.FullSizeContentView],
25 | titlebarAppearsTransparent: true,
26 | height: 618,
27 | width: 400,
28 | },
29 | },
30 | },
31 | DebugMenu: {
32 | component: DebugMenu,
33 | options: {
34 | title: 'Debug Menu',
35 | windowStyle: {
36 | height: 600,
37 | width: 800,
38 | },
39 | },
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/apps/menu-bar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@react-native/typescript-config/tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["es2019", "dom"]
5 | },
6 | "exclude": ["node_modules", "electron"]
7 | }
8 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 5.4.0"
4 | },
5 | "build": {
6 | "development": {
7 | "developmentClient": true,
8 | "distribution": "internal"
9 | },
10 | "preview": {
11 | "distribution": "internal"
12 | },
13 | "production": {}
14 | },
15 | "submit": {
16 | "production": {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json",
3 | "version": "0.0.0",
4 | "npmClient": "yarn"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "monorepo",
4 | "version": "1.0.0",
5 | "workspaces": [
6 | "apps/*",
7 | "packages/*"
8 | ],
9 | "scripts": {
10 | "build": "lerna run build",
11 | "lint": "lerna run lint",
12 | "watch": "lerna run watch --stream --parallel",
13 | "typecheck": "lerna run typecheck",
14 | "postinstall": "patch-package"
15 | },
16 | "dependencies": {
17 | "@tsconfig/node12": "1.0.7"
18 | },
19 | "devDependencies": {
20 | "lerna": "^7.3.0",
21 | "patch-package": "^8.0.0",
22 | "postinstall-postinstall": "^2.1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/common-types/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: 'universe/node',
4 | ignorePatterns: ['build/**', 'node_modules/**'],
5 | };
6 |
--------------------------------------------------------------------------------
/packages/common-types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "common-types",
3 | "version": "1.0.0",
4 | "main": "build/index.js",
5 | "scripts": {
6 | "build": "tsc",
7 | "watch": "yarn build --watch --preserveWatchOutput",
8 | "lint": "eslint .",
9 | "typecheck": "tsc"
10 | },
11 | "devDependencies": {
12 | "eslint-config-universe": "^15.0.3",
13 | "typescript": "^5.8.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/common-types/src/InternalError.ts:
--------------------------------------------------------------------------------
1 | import { JSONObject } from '@expo/json-file';
2 |
3 | const ERROR_PREFIX = 'Error: ';
4 | export default class InternalError extends Error {
5 | override readonly name = 'InternalError';
6 | code: InternalErrorCode;
7 | details?: JSONObject;
8 |
9 | constructor(code: InternalErrorCode, message: string, details?: JSONObject) {
10 | super('');
11 |
12 | // If e.toString() was called to get `message` we don't want it to look
13 | // like "Error: Error:".
14 | if (message.startsWith(ERROR_PREFIX)) {
15 | message = message.substring(ERROR_PREFIX.length);
16 | }
17 |
18 | this.message = message;
19 | this.code = code;
20 | this.details = details;
21 | }
22 | }
23 |
24 | export type InternalErrorCode =
25 | | 'APPLE_APP_VERIFICATION_FAILED'
26 | | 'APPLE_DEVICE_LOCKED'
27 | | 'EXPO_GO_NOT_INSTALLED_ON_DEVICE'
28 | | 'INVALID_VERSION'
29 | | 'MULTIPLE_APPS_IN_TARBALL'
30 | | 'TOOL_CHECK_FAILED'
31 | | 'XCODE_COMMAND_LINE_TOOLS_NOT_INSTALLED'
32 | | 'XCODE_LICENSE_NOT_ACCEPTED'
33 | | 'XCODE_NOT_INSTALLED'
34 | | 'SIMCTL_NOT_AVAILABLE'
35 | | 'NO_DEVELOPMENT_BUILDS_AVAILABLE'
36 | | 'UNAUTHORIZED_ACCOUNT';
37 |
38 | export type MultipleAppsInTarballErrorDetails = {
39 | apps: {
40 | name: string;
41 | path: string;
42 | }[];
43 | };
44 |
--------------------------------------------------------------------------------
/packages/common-types/src/cli-commands/checkTools.ts:
--------------------------------------------------------------------------------
1 | export type FailureReason = {
2 | message: string;
3 | command?: string;
4 | };
5 |
6 | export type PlatformToolsCheck = {
7 | android?: {
8 | success: boolean;
9 | reason?: FailureReason;
10 | };
11 | ios?: {
12 | success: boolean;
13 | reason?: FailureReason;
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/packages/common-types/src/cli-commands/index.ts:
--------------------------------------------------------------------------------
1 | import * as CheckTools from './checkTools';
2 | import * as ListDevices from './listDevices';
3 | import { Platform } from './platform';
4 |
5 | export { Platform, ListDevices, CheckTools };
6 |
--------------------------------------------------------------------------------
/packages/common-types/src/cli-commands/listDevices.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AndroidConnectedDevice,
3 | AndroidEmulator,
4 | AppleConnectedDevice,
5 | IosSimulator,
6 | } from '../devices';
7 | import { Platform } from './platform';
8 |
9 | export type Device
= P extends Platform.Ios
10 | ? IosSimulator | AppleConnectedDevice
11 | : P extends Platform.Android
12 | ? AndroidConnectedDevice | AndroidEmulator
13 | : never;
14 |
15 | export type DevicesPerPlatform = {
16 | [P in Exclude]: {
17 | devices: Device[];
18 | error?: { code: string; message: string };
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/packages/common-types/src/cli-commands/platform.ts:
--------------------------------------------------------------------------------
1 | export enum Platform {
2 | Android = 'android',
3 | Ios = 'ios',
4 | All = 'all',
5 | }
6 |
--------------------------------------------------------------------------------
/packages/common-types/src/constants.ts:
--------------------------------------------------------------------------------
1 | const host = 'api.expo.dev';
2 | const origin = `https://${host}`;
3 | const websiteOrigin = 'https://expo.dev';
4 |
5 | export const Config = {
6 | api: {
7 | host,
8 | origin,
9 | },
10 | website: {
11 | origin: websiteOrigin,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/packages/common-types/src/devices.ts:
--------------------------------------------------------------------------------
1 | export interface AppleConnectedDevice {
2 | /** @example `00008101-001964A22629003A` */
3 | udid: string;
4 | /** @example `Evan's phone` */
5 | name: string;
6 | /** @example `iPhone13,4` */
7 | model: string;
8 | /** @example `device` */
9 | deviceType: 'device' | 'catalyst';
10 | /** @example `USB` */
11 | connectionType: 'USB' | 'Network';
12 | /** @example `15.4.1` */
13 | osVersion: string;
14 | osType: 'iOS';
15 | developerModeStatus?: 'enabled' | 'disabled';
16 | }
17 |
18 | export interface IosSimulator {
19 | runtime: string;
20 | osVersion: string;
21 | windowName: string;
22 | osType: 'iOS' | 'tvOS';
23 | state: 'Booted' | 'Shutdown';
24 | isAvailable: boolean;
25 | name: string;
26 | udid: string;
27 | lastBootedAt?: number;
28 | deviceType: 'simulator';
29 | }
30 |
31 | export interface AndroidEmulator {
32 | pid?: string;
33 | name: string;
34 | osType: 'Android';
35 | deviceType: 'emulator';
36 | state: 'Booted' | 'Shutdown';
37 | }
38 |
39 | export interface AndroidConnectedDevice {
40 | pid: string;
41 | model: string;
42 | name: string;
43 | osType: 'Android';
44 | deviceType: 'device';
45 | connectionType?: 'USB' | 'Network';
46 | }
47 |
48 | export type Device = AppleConnectedDevice | IosSimulator | AndroidEmulator | AndroidConnectedDevice;
49 |
--------------------------------------------------------------------------------
/packages/common-types/src/index.ts:
--------------------------------------------------------------------------------
1 | import InternalError, { InternalErrorCode } from './InternalError';
2 | import * as CliCommands from './cli-commands';
3 | import { Platform } from './cli-commands';
4 | import { Config } from './constants';
5 | import * as Devices from './devices';
6 | import * as StorageUtils from './storage';
7 |
8 | export { Devices, CliCommands, InternalError, InternalErrorCode, Platform, StorageUtils, Config };
9 |
--------------------------------------------------------------------------------
/packages/common-types/src/storage.ts:
--------------------------------------------------------------------------------
1 | export const MMKVInstanceId = 'mmkv.default';
2 | const AUTH_FILE_NAME = 'auth.json';
3 |
4 | export function getExpoOrbitDirectory(homedir: string) {
5 | return `${homedir}/.expo/orbit`;
6 | }
7 |
8 | export function userSettingsFile(homedir: string): string {
9 | return `${getExpoOrbitDirectory(homedir)}/${AUTH_FILE_NAME}`;
10 | }
11 |
12 | export type UserSettingsData = {
13 | sessionSecret?: string;
14 | envVars?: Record;
15 | };
16 |
--------------------------------------------------------------------------------
/packages/common-types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "rootDir": "src"
6 | },
7 | "include": ["src"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/eas-shared/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: 'universe/node',
4 | ignorePatterns: ['build/**', 'node_modules/**'],
5 | };
6 |
--------------------------------------------------------------------------------
/packages/eas-shared/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['@expo/babel-preset-cli'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/api/internal/Config.ts:
--------------------------------------------------------------------------------
1 | import getenv from 'getenv';
2 |
3 | import * as Env from '../../env';
4 |
5 | interface ApiConfig {
6 | scheme: string;
7 | host: string;
8 | port: number | null;
9 | }
10 |
11 | interface XDLConfig {
12 | api: ApiConfig;
13 | developerTool: string;
14 | }
15 |
16 | function getAPI(): ApiConfig {
17 | if (Env.isLocal()) {
18 | return {
19 | scheme: 'http',
20 | host: 'localhost',
21 | port: 3000,
22 | };
23 | } else if (Env.isStaging()) {
24 | return {
25 | scheme: getenv.string('XDL_SCHEME', 'https'),
26 | host: 'staging.exp.host',
27 | port: getenv.int('XDL_PORT', 0) || null,
28 | };
29 | } else {
30 | return {
31 | scheme: getenv.string('XDL_SCHEME', 'https'),
32 | host: getenv.string('XDL_HOST', 'exp.host'),
33 | port: getenv.int('XDL_PORT', 0) || null,
34 | };
35 | }
36 | }
37 |
38 | const config: XDLConfig = {
39 | api: getAPI(),
40 | developerTool: 'expo-cli',
41 | };
42 |
43 | export default config;
44 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/api/internal/ConnectionStatus.ts:
--------------------------------------------------------------------------------
1 | let offline: boolean = false;
2 |
3 | export function setIsOffline(bool: boolean): void {
4 | offline = bool;
5 | }
6 |
7 | export function isOffline(): boolean {
8 | return offline;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/api/internal/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Config } from './Config';
2 | export * as ConnectionStatus from './ConnectionStatus';
3 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/downloadApkAsync.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 |
4 | import { downloadAppAsync } from './downloadAppAsync';
5 | import UserSettings from './userSettings';
6 | import * as Versions from './versions';
7 |
8 | function _apkCacheDirectory() {
9 | const dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory();
10 | const dir = path.join(dotExpoHomeDirectory, 'android-apk-cache');
11 | fs.mkdirpSync(dir);
12 | return dir;
13 | }
14 |
15 | export async function downloadApkAsync(
16 | url: string,
17 | downloadProgressCallback?: (roundedProgress: number) => void
18 | ) {
19 | if (!url) {
20 | const versions = await Versions.versionsAsync();
21 | url = versions.androidUrl;
22 | }
23 |
24 | const filename = path.parse(url).name;
25 | const apkPath = path.join(_apkCacheDirectory(), `${filename}.apk`);
26 |
27 | if (await fs.pathExists(apkPath)) {
28 | return apkPath;
29 | }
30 |
31 | await downloadAppAsync(url, apkPath, undefined, downloadProgressCallback);
32 | return apkPath;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/env.ts:
--------------------------------------------------------------------------------
1 | import getenv from 'getenv';
2 |
3 | export function isDebug(): boolean {
4 | return getenv.boolish('EXPO_DEBUG', false);
5 | }
6 |
7 | export function isStaging(): boolean {
8 | return getenv.boolish('EXPO_STAGING', false);
9 | }
10 |
11 | export function isLocal(): boolean {
12 | return getenv.boolish('EXPO_LOCAL', false);
13 | }
14 |
15 | export function isMenuBar(): boolean {
16 | return getenv.boolish('EXPO_MENU_BAR', false);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/fetch.ts:
--------------------------------------------------------------------------------
1 | import https from 'https';
2 | import fetch, { RequestInfo, RequestInit, Response } from 'node-fetch';
3 | import { systemCertsSync } from 'system-ca';
4 | export { Response, RequestInit } from 'node-fetch';
5 |
6 | let ca: string[] | undefined = undefined;
7 | try {
8 | ca = systemCertsSync({ includeNodeCertificates: true });
9 | } catch {}
10 | const agent = new https.Agent({
11 | ca,
12 | });
13 |
14 | export class RequestError extends Error {
15 | constructor(
16 | message: string,
17 | public readonly response: Response
18 | ) {
19 | super(message);
20 | }
21 | }
22 |
23 | export default async function (url: RequestInfo, init?: RequestInit): Promise {
24 | const response = await fetch(url, {
25 | agent,
26 | ...init,
27 | });
28 | if (response.status >= 400) {
29 | throw new RequestError(`Request failed: ${response.status} (${response.statusText})`, response);
30 | }
31 | return response;
32 | }
33 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/files.ts:
--------------------------------------------------------------------------------
1 | export function formatBytes(bytes: number): string {
2 | if (bytes === 0) {
3 | return `0`;
4 | }
5 | let multiplier = 1;
6 | if (bytes < 1024 * multiplier) {
7 | return `${Math.floor(bytes)} B`;
8 | }
9 | multiplier *= 1024;
10 | if (bytes < 102.4 * multiplier) {
11 | return `${(bytes / multiplier).toFixed(1)} KB`;
12 | }
13 | if (bytes < 1024 * multiplier) {
14 | return `${Math.floor(bytes / 1024)} KB`;
15 | }
16 | multiplier *= 1024;
17 | if (bytes < 102.4 * multiplier) {
18 | return `${(bytes / multiplier).toFixed(1)} MB`;
19 | }
20 | if (bytes < 1024 * multiplier) {
21 | return `${Math.floor(bytes / multiplier)} MB`;
22 | }
23 | multiplier *= 1024;
24 | if (bytes < 102.4 * multiplier) {
25 | return `${(bytes / multiplier).toFixed(1)} GB`;
26 | }
27 | return `${Math.floor(bytes / 1024)} GB`;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | downloadAndMaybeExtractAppAsync,
3 | AppPlatform,
4 | extractAppFromLocalArchiveAsync,
5 | } from './download';
6 | import * as Env from './env';
7 | import * as ManifestUtils from './manifest';
8 | import { Manifest } from './manifest';
9 | import { runAppOnIosSimulatorAsync, runAppOnAndroidEmulatorAsync, detectIOSAppType } from './run';
10 | import * as Emulator from './run/android/emulator';
11 | import { assertExecutablesExistAsync as validateAndroidSystemRequirementsAsync } from './run/android/systemRequirements';
12 | import AppleDevice from './run/ios/device';
13 | import * as Simulator from './run/ios/simulator';
14 | import { validateSystemRequirementsAsync as validateIOSSystemRequirementsAsync } from './run/ios/systemRequirements';
15 |
16 | export {
17 | AppPlatform,
18 | downloadAndMaybeExtractAppAsync,
19 | extractAppFromLocalArchiveAsync,
20 | runAppOnIosSimulatorAsync,
21 | runAppOnAndroidEmulatorAsync,
22 | validateAndroidSystemRequirementsAsync,
23 | validateIOSSystemRequirementsAsync,
24 | detectIOSAppType,
25 | Emulator,
26 | Simulator,
27 | AppleDevice,
28 | Env,
29 | ManifestUtils,
30 | Manifest,
31 | };
32 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/paths.ts:
--------------------------------------------------------------------------------
1 | import envPaths from 'env-paths';
2 |
3 | // Paths for storing things like data, config, cache, etc.
4 | // Should use the correct OS-specific paths (e.g. XDG base directory on Linux)
5 | const {
6 | data: DATA_PATH,
7 | config: CONFIG_PATH,
8 | cache: CACHE_PATH,
9 | log: LOG_PATH,
10 | temp: TEMP_PATH,
11 | } = envPaths('expo-orbit');
12 |
13 | export const getDataDirectory = (): string => DATA_PATH;
14 | export const getConfigDirectory = (): string => CONFIG_PATH;
15 | export const getCacheDirectory = (): string => CACHE_PATH;
16 | export const getLogDirectory = (): string => LOG_PATH;
17 | export const getTmpDirectory = (): string => TEMP_PATH;
18 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/android/sdk.ts:
--------------------------------------------------------------------------------
1 | import { pathExists } from 'fs-extra';
2 | import os from 'os';
3 | import path from 'path';
4 |
5 | export const ANDROID_DEFAULT_LOCATION: Readonly>> = {
6 | darwin: path.join(os.homedir(), 'Library', 'Android', 'sdk'),
7 | linux: path.join(os.homedir(), 'Android', 'Sdk'),
8 | win32: path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
9 | };
10 |
11 | const ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM = ANDROID_DEFAULT_LOCATION[process.platform];
12 |
13 | export async function getAndroidSdkRootAsync(): Promise {
14 | if (process.env.ANDROID_HOME && (await pathExists(process.env.ANDROID_HOME))) {
15 | return process.env.ANDROID_HOME;
16 | } else if (process.env.ANDROID_SDK_ROOT && (await pathExists(process.env.ANDROID_SDK_ROOT))) {
17 | return process.env.ANDROID_SDK_ROOT;
18 | } else if (
19 | ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM &&
20 | (await pathExists(ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM))
21 | ) {
22 | return ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM;
23 | } else {
24 | return null;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/android/systemRequirements.ts:
--------------------------------------------------------------------------------
1 | import spawnAsync from '@expo/spawn-async';
2 | import chalk from 'chalk';
3 | import { InternalError } from 'common-types';
4 |
5 | import { getAaptExecutableAsync } from './aapt';
6 | import { getAdbExecutableAsync } from './adb';
7 | import { getEmulatorExecutableAsync } from './emulator';
8 |
9 | async function assertExecutableExistsAsync(executable: string, options?: string[]): Promise {
10 | try {
11 | await spawnAsync(executable, options);
12 | } catch (err: any) {
13 | throw new InternalError(
14 | 'TOOL_CHECK_FAILED',
15 | `${chalk.bold(
16 | executable
17 | )} executable doesn't seem to work. Please make sure Android Studio is installed on your device and ${chalk.bold(
18 | 'ANDROID_HOME'
19 | )} or ${chalk.bold('ANDROID_SDK_ROOT')} env variables are set.
20 | ${err.message}`
21 | );
22 | }
23 | }
24 |
25 | export async function assertExecutablesExistAsync(): Promise {
26 | await assertExecutableExistsAsync(await getAdbExecutableAsync(), ['--version']);
27 | await assertExecutableExistsAsync(await getEmulatorExecutableAsync(), ['-list-avds']);
28 | await assertExecutableExistsAsync(await getAaptExecutableAsync(), ['version']);
29 | }
30 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/SimControl.ts:
--------------------------------------------------------------------------------
1 | type DeviceState = 'Shutdown' | 'Booted';
2 |
3 | export type SimulatorDevice = {
4 | availabilityError: 'runtime profile not found';
5 | /**
6 | * '/Users/name/Library/Developer/CoreSimulator/Devices/00E55DC0-0364-49DF-9EC6-77BE587137D4/data'
7 | */
8 | dataPath: string;
9 | /**
10 | * '/Users/name/Library/Logs/CoreSimulator/00E55DC0-0364-49DF-9EC6-77BE587137D4'
11 | */
12 | logPath: string;
13 | /**
14 | * '00E55DC0-0364-49DF-9EC6-77BE587137D4'
15 | */
16 | udid: string;
17 | /**
18 | * com.apple.CoreSimulator.SimRuntime.tvOS-13-4
19 | */
20 | runtime: string;
21 | isAvailable: boolean;
22 | /**
23 | * 'com.apple.CoreSimulator.SimDeviceType.Apple-TV-1080p'
24 | */
25 | deviceTypeIdentifier: string;
26 | state: DeviceState;
27 | /**
28 | * 'Apple TV'
29 | */
30 | name: string;
31 |
32 | osType: OSType;
33 | /**
34 | * '13.4'
35 | */
36 | osVersion: string;
37 | /**
38 | * 'iPhone 11 (13.6)'
39 | */
40 | windowName: string;
41 | };
42 |
43 | export type XCTraceDevice = {
44 | /**
45 | * '00E55DC0-0364-49DF-9EC6-77BE587137D4'
46 | */
47 | udid: string;
48 | /**
49 | * 'Apple TV'
50 | */
51 | name: string;
52 |
53 | deviceType: 'device' | 'catalyst';
54 | /**
55 | * '13.4'
56 | */
57 | osVersion: string;
58 | };
59 |
60 | type OSType = 'iOS' | 'tvOS' | 'watchOS' | 'macOS';
61 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/appleDevice/client/ServiceClient.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2021 Expo, Inc.
3 | * Copyright (c) 2018 Drifty Co.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | import { Socket } from 'net';
9 |
10 | import { CommandError } from '../../../../utils/errors';
11 | import { ProtocolClient } from '../protocol/AbstractProtocol';
12 |
13 | export abstract class ServiceClient {
14 | constructor(
15 | public socket: Socket,
16 | protected protocolClient: T
17 | ) {}
18 | }
19 |
20 | export class ResponseError extends CommandError {
21 | constructor(
22 | msg: string,
23 | public response: any
24 | ) {
25 | super(msg);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/constants.ts:
--------------------------------------------------------------------------------
1 | export const EXPO_GO_BUNDLE_IDENTIFIER = 'host.exp.Exponent';
2 | export const APP_STORE_BUNDLE_IDENTIFIER = 'com.apple.AppStore';
3 |
4 | export const EXPO_GO_APP_STORE_URL = 'https://apps.apple.com/br/app/expo-go/id982107779';
5 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/device.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getConnectedDevicesAsync,
3 | getBundleIdentifierForBinaryAsync,
4 | openURLAsync,
5 | openExpoGoURLAsync,
6 | ensureExpoClientInstalledAsync,
7 | checkIfAppIsInstalled,
8 | } from './appleDevice/AppleDevice';
9 | import { getAppDeltaDirectory, installOnDeviceAsync } from './appleDevice/installOnDeviceAsync';
10 |
11 | const AppleDevice = {
12 | getConnectedDevicesAsync,
13 | getAppDeltaDirectory,
14 | installOnDeviceAsync,
15 | getBundleIdentifierForBinaryAsync,
16 | openURLAsync,
17 | openExpoGoURLAsync,
18 | ensureExpoClientInstalledAsync,
19 | checkIfAppIsInstalled,
20 | };
21 |
22 | export default AppleDevice;
23 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/inspectApp.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 |
4 | import { parseBinaryPlistAsync } from '../../utils/parseBinaryPlistAsync';
5 |
6 | export async function detectIOSAppType(appPath: string): Promise<'device' | 'simulator'> {
7 | const builtInfoPlistPath = path.join(appPath, 'Info.plist');
8 | if (!fs.existsSync(builtInfoPlistPath)) {
9 | return 'device';
10 | }
11 |
12 | const { DTPlatformName }: { DTPlatformName: string } =
13 | await parseBinaryPlistAsync(builtInfoPlistPath);
14 |
15 | return DTPlatformName.includes('simulator') ? 'simulator' : 'device';
16 | }
17 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/simctl.ts:
--------------------------------------------------------------------------------
1 | import { SpawnOptions, SpawnResult } from '@expo/spawn-async';
2 |
3 | import { xcrunAsync } from './xcrun';
4 |
5 | export async function simctlAsync(args: string[], options?: SpawnOptions): Promise {
6 | return await xcrunAsync(['simctl', ...args], options);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/run/ios/xcode.ts:
--------------------------------------------------------------------------------
1 | import spawnAsync from '@expo/spawn-async';
2 | import chalk from 'chalk';
3 | import semver from 'semver';
4 |
5 | import Log from '../../log';
6 |
7 | // Based on the RN docs (Aug 2020).
8 | export const MIN_XCODE_VERSION = '9.4.0';
9 | export const APP_STORE_ID = '497799835';
10 |
11 | export async function getXcodeVersionAsync(): Promise {
12 | try {
13 | const { stdout } = await spawnAsync('xcodebuild', ['-version']);
14 |
15 | const version = stdout.match(/Xcode (\d+\.\d+)/)?.[1];
16 |
17 | const semverFormattedVersion = `${version}.0`;
18 |
19 | if (!semver.valid(semverFormattedVersion)) {
20 | Log.warn(
21 | `Xcode version ${chalk.bold(version)} is in unknown format. Expected format is ${chalk.bold(
22 | '12.0'
23 | )}.`
24 | );
25 | return undefined;
26 | }
27 |
28 | return semverFormattedVersion;
29 | } catch {
30 | // not installed
31 | return undefined;
32 | }
33 | }
34 |
35 | export async function openAppStoreAsync(appId: string): Promise {
36 | const link = getAppStoreLink(appId);
37 | await spawnAsync(`open`, [link]);
38 | }
39 |
40 | function getAppStoreLink(appId: string): string {
41 | if (process.platform === 'darwin') {
42 | // TODO: Is there ever a case where the macappstore isn't available on mac?
43 | return `macappstore://itunes.apple.com/app/id${appId}`;
44 | }
45 | return `https://apps.apple.com/us/app/id${appId}`;
46 | }
47 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/timer.ts:
--------------------------------------------------------------------------------
1 | const LABEL = 'DEFAULT';
2 | const startTimes: Record = {};
3 |
4 | export function hasTimer(label: string): number | null {
5 | return startTimes[label] ?? null;
6 | }
7 |
8 | export function startTimer(label = LABEL): void {
9 | startTimes[label] = Date.now();
10 | }
11 |
12 | export function endTimer(label = LABEL, clear: boolean = true): number {
13 | const endTime = Date.now();
14 | const startTime = startTimes[label];
15 | if (startTime) {
16 | const delta = endTime - startTime;
17 | if (clear) {
18 | delete startTimes[label];
19 | }
20 | return delta;
21 | }
22 | throw new Error(`Timer '${label}' has not be started yet`);
23 | }
24 |
25 | /**
26 | * Optimally format milliseconds
27 | *
28 | * @example `1h 2m 3s`
29 | * @example `5m 18s`
30 | * @example `40s`
31 | * @param duration
32 | */
33 | export function formatMilliseconds(duration: number): string {
34 | const portions: string[] = [];
35 |
36 | const msInHour = 1000 * 60 * 60;
37 | const hours = Math.trunc(duration / msInHour);
38 | if (hours > 0) {
39 | portions.push(hours + 'h');
40 | duration = duration - hours * msInHour;
41 | }
42 |
43 | const msInMinute = 1000 * 60;
44 | const minutes = Math.trunc(duration / msInMinute);
45 | if (minutes > 0) {
46 | portions.push(minutes + 'm');
47 | duration = duration - minutes * msInMinute;
48 | }
49 |
50 | const seconds = Math.trunc(duration / 1000);
51 | if (seconds > 0) {
52 | portions.push(seconds + 's');
53 | }
54 |
55 | return portions.join(' ');
56 | }
57 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/utils/delayAsync.ts:
--------------------------------------------------------------------------------
1 | export function delayAsync(ms: number): Promise {
2 | return new Promise((resolve) => setTimeout(resolve, ms));
3 | }
4 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/utils/dir.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | export function directoryExistsSync(file: string): boolean {
4 | try {
5 | return fs.statSync(file)?.isDirectory() ?? false;
6 | } catch {
7 | return false;
8 | }
9 | }
10 |
11 | export async function directoryExistsAsync(file: string): Promise {
12 | return (await fs.promises.stat(file).catch(() => null))?.isDirectory() ?? false;
13 | }
14 |
15 | export async function fileExistsAsync(file: string): Promise {
16 | return (await fs.promises.stat(file).catch(() => null))?.isFile() ?? false;
17 | }
18 |
19 | export const ensureDirectoryAsync = (path: string) => fs.promises.mkdir(path, { recursive: true });
20 |
21 | export const ensureDirectory = (path: string) => fs.mkdirSync(path, { recursive: true });
22 |
23 | export const copySync = fs.copySync;
24 |
25 | export const copyAsync = fs.copy;
26 |
27 | export const removeAsync = fs.remove;
28 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/utils/fn.ts:
--------------------------------------------------------------------------------
1 | /** `lodash.memoize` */
2 | export function memoize any>(fn: T): T {
3 | const cache: { [key: string]: any } = {};
4 | return ((...args: any[]) => {
5 | const key = JSON.stringify(args);
6 | if (cache[key]) {
7 | return cache[key];
8 | }
9 | const result = fn(...args);
10 | cache[key] = result;
11 | return result;
12 | }) as any;
13 | }
14 |
15 | /** memoizes an async function to prevent subsequent calls that might be invoked before the function has finished resolving. */
16 | export function guardAsync Promise>(fn: T): T {
17 | let invoked = false;
18 | let returnValue: V;
19 |
20 | const guard: any = async (...args: any[]): Promise => {
21 | if (!invoked) {
22 | invoked = true;
23 | returnValue = await fn(...args);
24 | }
25 |
26 | return returnValue;
27 | };
28 |
29 | return guard;
30 | }
31 |
32 | export function uniqBy(array: T[], key: (item: T) => string): T[] {
33 | const seen: { [key: string]: boolean } = {};
34 | return array.filter((item) => {
35 | const k = key(item);
36 | if (seen[k]) {
37 | return false;
38 | }
39 | seen[k] = true;
40 | return true;
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/utils/parseBinaryPlistAsync.ts:
--------------------------------------------------------------------------------
1 | import plist from '@expo/plist';
2 | import binaryPlist from 'bplist-parser';
3 | import fs from 'fs';
4 |
5 | const CHAR_CHEVRON_OPEN = 60;
6 | const CHAR_B_LOWER = 98;
7 | // .mobileprovision
8 | // const CHAR_ZERO = 30;
9 |
10 | export async function parseBinaryPlistAsync(plistPath: string) {
11 | return parsePlistBuffer(await fs.promises.readFile(plistPath));
12 | }
13 |
14 | export function parsePlistBuffer(contents: Buffer) {
15 | if (contents[0] === CHAR_CHEVRON_OPEN) {
16 | const info = plist.parse(contents.toString());
17 | if (Array.isArray(info)) return info[0];
18 | return info;
19 | } else if (contents[0] === CHAR_B_LOWER) {
20 | const info = binaryPlist.parseBuffer(contents);
21 | if (Array.isArray(info)) return info[0];
22 | return info;
23 | } else {
24 | throw new Error(`Cannot parse plist of type byte (0x${contents[0].toString(16)})`);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/eas-shared/src/utils/promise.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns a promise that will be resolved after given ms milliseconds.
3 | *
4 | * @param ms A number of milliseconds to sleep.
5 | * @returns A promise that resolves after the provided number of milliseconds.
6 | */
7 | export async function sleepAsync(ms: number): Promise {
8 | return new Promise((res) => setTimeout(res, ms));
9 | }
10 |
--------------------------------------------------------------------------------
/packages/eas-shared/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "build/cjs"
6 | },
7 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/eas-shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "importHelpers": true,
5 | "esModuleInterop": true,
6 | "noFallthroughCasesInSwitch": true,
7 | "noImplicitOverride": true,
8 | "noImplicitReturns": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "strict": true,
12 | "outDir": "build/esm",
13 | "rootDir": "src",
14 | "allowSyntheticDefaultImports": true,
15 | "lib": ["ES2022"],
16 | "module": "ES2022",
17 | "moduleResolution": "node",
18 | "target": "ES2022",
19 | "typeRoots": ["../../ts-declarations", "src/ts-declarations", "./node_modules/@types"]
20 | },
21 | "include": ["src/**/*"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: 'universe/node',
4 | ignorePatterns: ['build/**', 'node_modules/**'],
5 | };
6 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-electron-modules",
3 | "version": "1.0.0",
4 | "main": "build/index.js",
5 | "scripts": {
6 | "build": "tsc",
7 | "watch": "yarn build --watch --preserveWatchOutput",
8 | "lint": "eslint .",
9 | "typecheck": "tsc"
10 | },
11 | "devDependencies": {
12 | "electron": "28.2.0",
13 | "eslint-config-universe": "^15.0.3",
14 | "typescript": "^5.8.3"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './registerElectronModules';
2 | export * from './exposeElectronModules';
3 | export * from './requireElectronModule';
4 | export * from './types';
5 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/src/registerElectronModules.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron';
2 |
3 | import { ElectronModule, IpcMainModules, Registry } from './types';
4 |
5 | const ipcMainModules: IpcMainModules = {};
6 |
7 | export function registerMainModules(modules: Registry) {
8 | modules.forEach((module) => {
9 | registerMainModule(module);
10 | });
11 |
12 | ipcMain.on('get-all-ipc-main-modules', (event) => {
13 | event.returnValue = ipcMainModules;
14 | });
15 | }
16 |
17 | function registerMainModule(module: ElectronModule) {
18 | ipcMainModules[module.name] = { functions: [], values: [] };
19 |
20 | Object.entries(module).forEach(([key, value]) => {
21 | const moduleFunctionKey = `${module.name}:${key}`;
22 | if (typeof value === 'function') {
23 | // Adds a handler for an invokeable IPC and send IpcMainInvokeEvent as the last argument
24 | ipcMain.handle(moduleFunctionKey, (event, ...args) => value(...args, event));
25 | ipcMainModules[module.name].functions.push(key);
26 | } else {
27 | // No need to add a handler for the module name
28 | if (key === 'name') return;
29 |
30 | ipcMain.on(moduleFunctionKey, (event) => {
31 | event.returnValue = value;
32 | });
33 | ipcMainModules[module.name].values.push(key);
34 | }
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/src/requireElectronModule.ts:
--------------------------------------------------------------------------------
1 | import { ElectronModule } from './types';
2 |
3 | type ReactNativeElectronModulesObject = {
4 | modules: {
5 | [moduleName: string]: ElectronModule;
6 | };
7 | };
8 |
9 | declare global {
10 | // eslint-disable-next-line no-var
11 | var __reactNativeElectronModules: ReactNativeElectronModulesObject | undefined;
12 | }
13 |
14 | /**
15 | * Imports a module registered with the given name.
16 | *
17 | * @param moduleName Name of the requested native module.
18 | * @returns Object representing the electron module.
19 | * @throws Error when there is no electron module with given name.
20 | */
21 | export function requireElectronModule(moduleName: string): ModuleType {
22 | const nativeModule =
23 | (globalThis.__reactNativeElectronModules?.modules?.[moduleName] as ModuleType) ?? null;
24 |
25 | if (!nativeModule) {
26 | throw new Error(`Cannot find electron module '${moduleName}'`);
27 | }
28 | return nativeModule;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/src/types.ts:
--------------------------------------------------------------------------------
1 | export type ElectronModule = {
2 | name: string;
3 | [key: string]:
4 | | number
5 | | string
6 | | boolean
7 | | ((...args: any[]) => Promise | any)
8 | | { [key: string]: number | string | boolean };
9 | };
10 |
11 | export type Registry = ElectronModule[];
12 |
13 | export type IpcMainModules = {
14 | [moduleName: string]: { functions: string[]; values: string[] };
15 | };
16 |
--------------------------------------------------------------------------------
/packages/react-native-electron-modules/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "rootDir": "src",
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler"
8 | },
9 | "include": ["src"]
10 | }
11 |
--------------------------------------------------------------------------------
/ts-declarations/exec-async/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'exec-async' {
2 | import { ExecFileOptions } from 'child_process';
3 |
4 | export type ExecAsyncOptions = ExecFileOptions;
5 |
6 | export default function execAsync(
7 | command: string,
8 | args?: readonly string[] | object | undefined,
9 | options?: ExecAsyncOptions
10 | ): Promise;
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node12/tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "composite": true,
6 | "inlineSources": true,
7 | "moduleResolution": "node",
8 | "noImplicitReturns": true,
9 | "resolveJsonModule": true,
10 | "sourceMap": true,
11 | "typeRoots": ["./ts-declarations", "./node_modules/@types", "../../node_modules/@types", "../../ts-declarations"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------