Merge branch 'main' of github.com:facebook/flipper into universalBuild
This commit is contained in:
@@ -13,5 +13,6 @@ react-native/ReactNativeFlipperExample
|
||||
scripts/generate-changelog.js
|
||||
static/index.js
|
||||
static/defaultPlugins/*
|
||||
static/facebook/flipper-server-app-template
|
||||
generated
|
||||
flipper-server/static
|
||||
|
||||
@@ -204,7 +204,7 @@ module.exports = {
|
||||
'no-dupe-class-members': 0,
|
||||
'@typescript-eslint/no-redeclare': 1,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
1,
|
||||
2,
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
varsIgnorePattern: '^_',
|
||||
|
||||
1
desktop/.gitignore
vendored
1
desktop/.gitignore
vendored
@@ -12,3 +12,4 @@ tsc-error.log
|
||||
/flipper-server/static/
|
||||
/static/flipper-server-log*
|
||||
/static/.audit.json
|
||||
/static/icons/*_d.png
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
checkPortInUse,
|
||||
getAuthToken,
|
||||
getEnvironmentInfo,
|
||||
hasAuthToken,
|
||||
setupPrefetcher,
|
||||
startFlipperServer,
|
||||
startServer,
|
||||
@@ -112,19 +111,6 @@ async function getFlipperServer(
|
||||
);
|
||||
const keytar: KeytarModule | undefined = await getKeytarModule(staticPath);
|
||||
const port = 52342;
|
||||
/**
|
||||
* Only attempt to use the auth token if one is available. Otherwise,
|
||||
* trying to get the auth token will try to generate one if it does not exist.
|
||||
* At this state, it would be impossible to generate it as our certificates
|
||||
* may not be available yet.
|
||||
*/
|
||||
let token: string | undefined;
|
||||
if (await hasAuthToken()) {
|
||||
token = await getAuthToken();
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(token ? {token} : {});
|
||||
const TCPconnectionURL = new URL(`ws://localhost:${port}?${searchParams}`);
|
||||
|
||||
async function shutdown(): Promise<boolean> {
|
||||
console.info('[flipper-server] Attempt to shutdown.');
|
||||
@@ -173,6 +159,10 @@ async function getFlipperServer(
|
||||
environmentInfo,
|
||||
);
|
||||
|
||||
const token: string = await getAuthToken();
|
||||
const searchParams = new URLSearchParams({token});
|
||||
const TCPconnectionURL = new URL(`ws://localhost:${port}?${searchParams}`);
|
||||
|
||||
const companionEnv = await initCompanionEnv(server);
|
||||
await server.connect();
|
||||
await readyForIncomingConnections(server, companionEnv);
|
||||
|
||||
@@ -19,9 +19,8 @@ test('filled icons get correct local path', () => {
|
||||
name: 'star',
|
||||
variant: 'filled',
|
||||
size: 12,
|
||||
density: 2,
|
||||
});
|
||||
expect(iconPath).toBe(path.join('icons', 'star-filled-12@2x.png'));
|
||||
expect(iconPath).toBe(path.join('icons', 'star-filled_d.png'));
|
||||
});
|
||||
|
||||
test('outline icons get correct local path', () => {
|
||||
@@ -29,9 +28,8 @@ test('outline icons get correct local path', () => {
|
||||
name: 'star',
|
||||
variant: 'outline',
|
||||
size: 12,
|
||||
density: 2,
|
||||
});
|
||||
expect(iconPath).toBe(path.join('icons', 'star-outline-12@2x.png'));
|
||||
expect(iconPath).toBe(path.join('icons', 'star-outline_d.png'));
|
||||
});
|
||||
|
||||
test('filled icons get correct URL', async () => {
|
||||
@@ -39,11 +37,10 @@ test('filled icons get correct URL', async () => {
|
||||
name: 'star',
|
||||
variant: 'filled',
|
||||
size: 12,
|
||||
density: 2,
|
||||
} as const;
|
||||
const iconUrl = getPublicIconUrl(icon);
|
||||
expect(iconUrl).toBe(
|
||||
'https://facebook.com/images/assets_DO_NOT_HARDCODE/facebook_icons/star_filled_12.png', // TODO: support density?
|
||||
'https://facebook.com/images/assets_DO_NOT_HARDCODE/facebook_icons/star_filled_12.png',
|
||||
);
|
||||
const staticPath = getRenderHostInstance().serverConfig.paths.staticPath;
|
||||
const localUrl = getLocalIconUrl(icon, iconUrl, staticPath, false);
|
||||
@@ -51,7 +48,7 @@ test('filled icons get correct URL', async () => {
|
||||
expect(localUrl).toBe(iconUrl);
|
||||
|
||||
// ... let's mock a file
|
||||
const iconPath = path.join(staticPath, 'icons', 'star-filled-12@2x.png');
|
||||
const iconPath = path.join(staticPath, 'icons', 'star-filled_d.png');
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
iconPath,
|
||||
|
||||
@@ -23,7 +23,7 @@ let _icons: Icons | undefined;
|
||||
|
||||
function getIconsSync(staticPath: string): Icons {
|
||||
return (
|
||||
_icons! ??
|
||||
_icons ??
|
||||
(_icons = JSON.parse(
|
||||
fs.readFileSync(path.join(staticPath, 'icons.json'), {encoding: 'utf8'}),
|
||||
))
|
||||
@@ -31,10 +31,7 @@ function getIconsSync(staticPath: string): Icons {
|
||||
}
|
||||
|
||||
export function buildLocalIconPath(icon: Icon) {
|
||||
return path.join(
|
||||
'icons',
|
||||
`${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`,
|
||||
);
|
||||
return path.join('icons', `${icon.name}-${icon.variant}_d.png`);
|
||||
}
|
||||
|
||||
export function getLocalIconUrl(
|
||||
@@ -68,7 +65,7 @@ function tryRegisterIcon(icon: Icon, url: string, staticPath: string) {
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
`Trying to use icon '${entryName}' with size ${size} and density ${icon.density}, however the icon doesn't seem to exists at ${url}: ${res.status}`,
|
||||
`Trying to use icon '${entryName}' with size ${size}, however the icon doesn't seem to exists at ${url}: ${res.status}`,
|
||||
);
|
||||
}
|
||||
if (!existing.includes(size)) {
|
||||
|
||||
19
desktop/doctor/src/fb-stubs/validateSelectedXcodeVersion.tsx
Normal file
19
desktop/doctor/src/fb-stubs/validateSelectedXcodeVersion.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {FlipperDoctor} from 'flipper-common';
|
||||
|
||||
export async function validateSelectedXcodeVersion(
|
||||
_selectedPath: string,
|
||||
): Promise<FlipperDoctor.HealthcheckRunResult> {
|
||||
return {
|
||||
hasProblem: false,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import * as path from 'path';
|
||||
import type {FlipperDoctor} from 'flipper-common';
|
||||
import * as fs_extra from 'fs-extra';
|
||||
import {getIdbInstallationInstructions} from './fb-stubs/idbInstallationInstructions';
|
||||
import {validateSelectedXcodeVersion} from './fb-stubs/validateSelectedXcodeVersion';
|
||||
|
||||
export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
return {
|
||||
@@ -177,25 +178,39 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
isRequired: true,
|
||||
run: async (_: FlipperDoctor.EnvironmentInfo) => {
|
||||
const result = await tryExecuteCommand('xcode-select -p');
|
||||
const selectXcodeCommands = [
|
||||
{
|
||||
title: 'Select Xcode version',
|
||||
command: `sudo xcode-select -switch <path/to/>Xcode.app`,
|
||||
},
|
||||
];
|
||||
if (result.hasProblem) {
|
||||
return {
|
||||
hasProblem: true,
|
||||
message: `Xcode version is not selected. You can select it using command "sudo xcode-select -switch <path/to/>Xcode.app". ${result.message}.`,
|
||||
message: `Xcode version is not selected. ${result.message}.`,
|
||||
commands: selectXcodeCommands,
|
||||
};
|
||||
}
|
||||
const selectedXcode = result.stdout!.toString().trim();
|
||||
const selectedXcode = result.stdout.toString().trim();
|
||||
if (selectedXcode == '/Library/Developer/CommandLineTools') {
|
||||
return {
|
||||
hasProblem: true,
|
||||
message: `xcode-select has no Xcode selected, You can select it using command "sudo xcode-select -switch <path/to/>Xcode.app".`,
|
||||
message: `xcode-select has no Xcode selected.`,
|
||||
commands: selectXcodeCommands,
|
||||
};
|
||||
}
|
||||
if ((await fs_extra.pathExists(selectedXcode)) == false) {
|
||||
return {
|
||||
hasProblem: true,
|
||||
message: `xcode-select has path of ${selectedXcode}, however this path does not exist on disk. Run "sudo xcode-select --switch" with a valid Xcode.app path.`,
|
||||
message: `xcode-select has path of ${selectedXcode}, however this path does not exist on disk.`,
|
||||
commands: selectXcodeCommands,
|
||||
};
|
||||
}
|
||||
const validatedXcodeVersion =
|
||||
await validateSelectedXcodeVersion(selectedXcode);
|
||||
if (validatedXcodeVersion.hasProblem) {
|
||||
return validatedXcodeVersion;
|
||||
}
|
||||
return {
|
||||
hasProblem: false,
|
||||
message: `xcode-select has path of ${selectedXcode}.`,
|
||||
|
||||
@@ -64,14 +64,28 @@ export namespace FlipperDoctor {
|
||||
) => Promise<HealthcheckRunResult>;
|
||||
};
|
||||
|
||||
export type CliCommand = {
|
||||
title: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
export type HealthcheckRunResult = {
|
||||
hasProblem: boolean;
|
||||
message: string;
|
||||
/**
|
||||
* Commands to show to mitigate a problem or hint for more information
|
||||
*/
|
||||
commands?: CliCommand[];
|
||||
};
|
||||
|
||||
export type SubprocessHealtcheckRunResult = HealthcheckRunResult & {
|
||||
stdout?: string;
|
||||
};
|
||||
export type SubprocessHealtcheckRunResult =
|
||||
| (HealthcheckRunResult & {
|
||||
hasProblem: true;
|
||||
})
|
||||
| (HealthcheckRunResult & {
|
||||
hasProblem: false;
|
||||
stdout: string;
|
||||
});
|
||||
|
||||
export type CategoryResult = [
|
||||
string,
|
||||
@@ -99,6 +113,7 @@ export namespace FlipperDoctor {
|
||||
status: HealthcheckStatus;
|
||||
isAcknowledged?: boolean;
|
||||
message?: string;
|
||||
commands?: CliCommand[];
|
||||
};
|
||||
|
||||
export type HealthcheckReportItem = {
|
||||
|
||||
@@ -380,6 +380,7 @@ export type FlipperServerCommands = {
|
||||
shutdown: () => Promise<void>;
|
||||
'is-logged-in': () => Promise<boolean>;
|
||||
'environment-info': () => Promise<EnvironmentInfo>;
|
||||
'move-pwa': () => Promise<void>;
|
||||
};
|
||||
|
||||
export type GraphResponse = {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"immer": "^9.0.18",
|
||||
"js-base64": "^3.7.5",
|
||||
"p-map": "^4.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -20,7 +20,6 @@ type Icon = {
|
||||
name: string;
|
||||
variant: 'outline' | 'filled';
|
||||
size: number;
|
||||
density: number;
|
||||
};
|
||||
|
||||
interface NotificationAction {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"@types/react-color": "2.13.5",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"dayjs": "^1.11.10",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"flipper-common": "0.0.0",
|
||||
"flipper-plugin-core": "0.0.0",
|
||||
"immer": "^9.0.18",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {CSSProperties, forwardRef} from 'react';
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
normalizePadding,
|
||||
|
||||
@@ -16,7 +16,6 @@ import React, {
|
||||
} from 'react';
|
||||
import {Badge, Tooltip, Typography, Button} from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import {keyframes} from '@emotion/css';
|
||||
import reactElementToJSXString from 'react-element-to-jsx-string';
|
||||
import {SandyPluginContext} from '../plugin/PluginContext';
|
||||
import {createState} from 'flipper-plugin-core';
|
||||
|
||||
@@ -29,6 +29,7 @@ export const Panel: React.FC<{
|
||||
pad?: Spacing;
|
||||
gap?: Spacing;
|
||||
extraActions?: React.ReactElement | null;
|
||||
className?: string;
|
||||
}> = (props) => {
|
||||
const [collapsed, setCollapsed] = useLocalStorageState(
|
||||
`panel:${props.title}:collapsed`,
|
||||
@@ -45,6 +46,7 @@ export const Panel: React.FC<{
|
||||
return (
|
||||
<TrackingScope scope={props.title}>
|
||||
<StyledCollapse
|
||||
className={props.className}
|
||||
bordered={false}
|
||||
activeKey={collapsed ? undefined : props.title}
|
||||
onChange={toggle}>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {theme} from '../theme';
|
||||
|
||||
const containerStyle = css`
|
||||
flex: 1 0 auto;
|
||||
background-color: ${theme.white};
|
||||
background-color: ${theme.backgroundDefault};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: ${theme.borderRadius};
|
||||
|
||||
@@ -32,6 +32,37 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
}));
|
||||
}, [enumLabels]);
|
||||
|
||||
const width = React.useMemo(() => {
|
||||
const minWidth = 100;
|
||||
const maxWidth = 250;
|
||||
|
||||
let longestOptionLabelWidth = 0;
|
||||
Object.values(enumLabels).forEach((label) => {
|
||||
if (label.length > longestOptionLabelWidth) {
|
||||
longestOptionLabelWidth = label.length;
|
||||
}
|
||||
});
|
||||
|
||||
// 10px is an emperically calculated multiplier.
|
||||
// A proper way to do it is to actually render the longest option and measure it
|
||||
// But then we will have to render top X longest options,
|
||||
// because, potentially, a string with X reeeeally wide chars could be longer than X+1 narrow chars.
|
||||
// Anyhow, it seems too complex for such a simple thing a detecting the width of the select and teh dropdown
|
||||
// (the dropdown is the one that actually matters from the UX perspective - users use it to select the option they want)
|
||||
// Feel to increase 10 to any other value if necessary (11?)
|
||||
const longestOptionsLabelWidthPx = longestOptionLabelWidth * 10;
|
||||
|
||||
if (longestOptionsLabelWidthPx < minWidth) {
|
||||
return minWidth;
|
||||
}
|
||||
|
||||
if (longestOptionsLabelWidthPx > maxWidth) {
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
return longestOptionsLabelWidthPx;
|
||||
}, [enumLabels]);
|
||||
|
||||
const selectValueRef = React.useRef<string>();
|
||||
if (defaultValue && !selectValueRef.current) {
|
||||
selectValueRef.current = defaultValue;
|
||||
@@ -41,7 +72,7 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
return (
|
||||
<Select
|
||||
autoFocus
|
||||
style={{width: 100}}
|
||||
style={{width}}
|
||||
placeholder="..."
|
||||
options={options}
|
||||
defaultOpen
|
||||
|
||||
@@ -35,6 +35,12 @@ export {PowerSearchConfig, OperatorConfig, FieldConfig, SearchExpressionTerm};
|
||||
|
||||
type PowerSearchProps = {
|
||||
config: PowerSearchConfig;
|
||||
// Overrides current state of the component with every update.
|
||||
// It is the way to continuously force update the state of the power search externally.
|
||||
// Takes prefernce over `initialSearchExpression`.
|
||||
searchExpression?: SearchExpressionTerm[];
|
||||
// Component stays uncontrolled and maintains its own state.
|
||||
// It is respected only on initialization and any future updates are ignored.
|
||||
initialSearchExpression?: SearchExpressionTerm[];
|
||||
onSearchExpressionChange: (searchExpression: SearchExpressionTerm[]) => void;
|
||||
onConfirmUnknownOption?: (
|
||||
@@ -46,13 +52,22 @@ const OPTION_KEY_DELIMITER = '::';
|
||||
|
||||
export const PowerSearch: React.FC<PowerSearchProps> = ({
|
||||
config,
|
||||
searchExpression: searchExpressionExternal,
|
||||
initialSearchExpression,
|
||||
onSearchExpressionChange,
|
||||
onConfirmUnknownOption,
|
||||
}) => {
|
||||
const [searchExpression, setSearchExpression] = React.useState<
|
||||
IncompleteSearchExpressionTerm[]
|
||||
>(initialSearchExpression ?? []);
|
||||
>(() => {
|
||||
if (searchExpressionExternal) {
|
||||
return searchExpressionExternal;
|
||||
}
|
||||
if (initialSearchExpression) {
|
||||
return initialSearchExpression;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const onSearchExpressionChangeLatestRef = useLatestRef(
|
||||
onSearchExpressionChange,
|
||||
@@ -69,6 +84,12 @@ export const PowerSearch: React.FC<PowerSearchProps> = ({
|
||||
}
|
||||
}, [searchExpression, onSearchExpressionChangeLatestRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchExpressionExternal) {
|
||||
setSearchExpression(searchExpressionExternal);
|
||||
}
|
||||
}, [searchExpressionExternal]);
|
||||
|
||||
const options: PowerSearchTermFinderOptionGroup[] = React.useMemo(() => {
|
||||
const groupedOptions: PowerSearchTermFinderOptionGroup[] = [];
|
||||
|
||||
@@ -112,7 +133,7 @@ export const PowerSearch: React.FC<PowerSearchProps> = ({
|
||||
{searchExpression.map((searchTerm, i) => {
|
||||
return (
|
||||
<PowerSearchTerm
|
||||
key={i.toString()}
|
||||
key={JSON.stringify(searchTerm)}
|
||||
searchTerm={searchTerm}
|
||||
onCancel={() => {
|
||||
setSearchExpression((prevSearchExpression) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export function Tabs({
|
||||
localStorageKeyOverride, //set this if you need to have a dynamic number of tabs, you do *not* need to namespace with the plugin name
|
||||
...baseProps
|
||||
}: {grow?: boolean; localStorageKeyOverride?: string} & TabsProps) {
|
||||
const keys: string[] = [];
|
||||
const keys: string[] = baseProps.items?.map((item) => item.key) ?? [];
|
||||
const keyedChildren = Children.map(children, (child: any, idx) => {
|
||||
if (!child || typeof child !== 'object') {
|
||||
return;
|
||||
@@ -53,13 +53,13 @@ export function Tabs({
|
||||
});
|
||||
|
||||
const [activeTab, setActiveTab] = useLocalStorageState<string | undefined>(
|
||||
'Tabs:' + localStorageKeyOverride ?? keys.join(','),
|
||||
'Tabs:' + (localStorageKeyOverride ?? keys.join(',')),
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<AntdTabs
|
||||
activeKey={activeTab}
|
||||
activeKey={keys.includes(activeTab ?? 'not-there') ? activeTab : keys[0]}
|
||||
onChange={(key) => {
|
||||
setActiveTab(key);
|
||||
}}
|
||||
|
||||
@@ -673,7 +673,7 @@ export function DataTable<T extends object>(
|
||||
<Searchbar gap>
|
||||
<PowerSearch
|
||||
config={powerSearchConfig}
|
||||
initialSearchExpression={searchExpression}
|
||||
searchExpression={searchExpression}
|
||||
onSearchExpressionChange={(newSearchExpression) => {
|
||||
tableManager.setSearchExpression(newSearchExpression);
|
||||
}}
|
||||
|
||||
@@ -7,15 +7,9 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {CSSProperties, forwardRef} from 'react';
|
||||
import React, {CSSProperties} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
normalizePadding,
|
||||
normalizeSpace,
|
||||
PaddingProps,
|
||||
Spacing,
|
||||
theme,
|
||||
} from './theme';
|
||||
import {normalizeSpace, Spacing, theme} from './theme';
|
||||
|
||||
import type {SplitLayoutProps} from './Layout';
|
||||
import {Sidebar} from './Sidebar';
|
||||
|
||||
@@ -74,7 +74,7 @@ export function initializeRenderHost(
|
||||
},
|
||||
getLocalIconUrl(icon, url) {
|
||||
if (isProduction()) {
|
||||
return `icons/${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`;
|
||||
return `icons/${icon.name}-${icon.variant}_d.png`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
"serialize-error": "^8.1.0",
|
||||
"split2": "^4.1.0",
|
||||
"tmp": "^0.2.1",
|
||||
"which": "^2.0.2",
|
||||
"ws": "^8.6.0",
|
||||
"xdg-basedir": "^4.0.0"
|
||||
},
|
||||
@@ -63,7 +62,6 @@
|
||||
"@types/rsocket-tcp-server": "^0.0.2",
|
||||
"@types/split2": "^4.2.0",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/which": "^2.0.1",
|
||||
"@types/ws": "^8.5.3",
|
||||
"mock-fs": "^5.2.0"
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ import {flipperDataFolder, flipperSettingsFolder} from './utils/paths';
|
||||
import {DebuggableDevice} from './devices/DebuggableDevice';
|
||||
import {jfUpload} from './fb-stubs/jf';
|
||||
import path from 'path';
|
||||
import {movePWA} from './utils/findInstallation';
|
||||
|
||||
const {access, copyFile, mkdir, unlink, stat, readlink, readFile, writeFile} =
|
||||
promises;
|
||||
@@ -597,6 +598,9 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
'environment-info': async () => {
|
||||
return this.config.environmentInfo;
|
||||
},
|
||||
'move-pwa': async () => {
|
||||
await movePWA();
|
||||
},
|
||||
};
|
||||
|
||||
registerDevice(device: ServerDevice) {
|
||||
|
||||
@@ -47,37 +47,41 @@ class ServerRSocket extends ServerWebSocketBase {
|
||||
start(port: number, sslConfig?: SecureServerConfig): Promise<number> {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let rawServer: RSocketServer<any, any> | undefined;
|
||||
const serverFactory = (onConnect: (socket: Socket) => void) => {
|
||||
const transportServer = sslConfig
|
||||
? tls.createServer(sslConfig, (socket) => {
|
||||
onConnect(socket);
|
||||
})
|
||||
: net.createServer(onConnect);
|
||||
transportServer.on('error', reject).on('listening', () => {
|
||||
console.debug(
|
||||
`${
|
||||
sslConfig ? 'Secure' : 'Certificate'
|
||||
} server started on port ${port}`,
|
||||
'server',
|
||||
);
|
||||
self.listener.onListening(port);
|
||||
self.rawServer_ = rawServer;
|
||||
resolve((transportServer.address() as AddressInfo).port);
|
||||
try {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let rawServer: RSocketServer<any, any> | undefined;
|
||||
const serverFactory = (onConnect: (socket: Socket) => void) => {
|
||||
const transportServer = sslConfig
|
||||
? tls.createServer(sslConfig, (socket) => {
|
||||
onConnect(socket);
|
||||
})
|
||||
: net.createServer(onConnect);
|
||||
transportServer.on('error', reject).on('listening', () => {
|
||||
console.debug(
|
||||
`${
|
||||
sslConfig ? 'Secure' : 'Certificate'
|
||||
} server started on port ${port}`,
|
||||
'server',
|
||||
);
|
||||
self.listener.onListening(port);
|
||||
self.rawServer_ = rawServer;
|
||||
resolve((transportServer.address() as AddressInfo).port);
|
||||
});
|
||||
return transportServer;
|
||||
};
|
||||
rawServer = new RSocketServer({
|
||||
getRequestHandler: sslConfig
|
||||
? this._trustedRequestHandler
|
||||
: this._untrustedRequestHandler,
|
||||
transport: new RSocketTCPServer({
|
||||
port: port,
|
||||
serverFactory: serverFactory,
|
||||
}),
|
||||
});
|
||||
return transportServer;
|
||||
};
|
||||
rawServer = new RSocketServer({
|
||||
getRequestHandler: sslConfig
|
||||
? this._trustedRequestHandler
|
||||
: this._untrustedRequestHandler,
|
||||
transport: new RSocketTCPServer({
|
||||
port: port,
|
||||
serverFactory: serverFactory,
|
||||
}),
|
||||
});
|
||||
rawServer.start();
|
||||
rawServer.start();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,46 +49,50 @@ class ServerWebSocket extends ServerWebSocketBase {
|
||||
|
||||
async start(port: number, sslConfig?: SecureServerConfig): Promise<number> {
|
||||
const assignedPort = await new Promise<number>((resolve, reject) => {
|
||||
const server = sslConfig
|
||||
? createHttpsServer(sslConfig)
|
||||
: createHttpServer();
|
||||
try {
|
||||
const server = sslConfig
|
||||
? createHttpsServer(sslConfig)
|
||||
: createHttpServer();
|
||||
|
||||
const wsServer = new WSServer({
|
||||
server,
|
||||
verifyClient: this.verifyClient(),
|
||||
maxPayload: WEBSOCKET_MAX_MESSAGE_SIZE,
|
||||
});
|
||||
const wsServer = new WSServer({
|
||||
server,
|
||||
verifyClient: this.verifyClient(),
|
||||
maxPayload: WEBSOCKET_MAX_MESSAGE_SIZE,
|
||||
});
|
||||
|
||||
// We do not need to listen to http server's `error` because it is propagated to WS
|
||||
// https://github.com/websockets/ws/blob/a3a22e4ed39c1a3be8e727e9c630dd440edc61dd/lib/websocket-server.js#L109
|
||||
const onConnectionError = (error: Error) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Unable to start server at port ${port} due to ${JSON.stringify(
|
||||
serializeError(error),
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
wsServer.once('error', onConnectionError);
|
||||
server.listen(port, () => {
|
||||
console.debug(
|
||||
`[ws] ${
|
||||
sslConfig ? 'Secure' : 'Insecure'
|
||||
} server started on port ${port}`,
|
||||
'server',
|
||||
);
|
||||
// We do not need to listen to http server's `error` because it is propagated to WS
|
||||
// https://github.com/websockets/ws/blob/a3a22e4ed39c1a3be8e727e9c630dd440edc61dd/lib/websocket-server.js#L109
|
||||
const onConnectionError = (error: Error) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Unable to start server at port ${port} due to ${JSON.stringify(
|
||||
serializeError(error),
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
wsServer.once('error', onConnectionError);
|
||||
server.listen(port, () => {
|
||||
console.debug(
|
||||
`[ws] ${
|
||||
sslConfig ? 'Secure' : 'Insecure'
|
||||
} server started on port ${port}`,
|
||||
'server',
|
||||
);
|
||||
|
||||
// Unsubscribe connection error listener.
|
||||
// We'll attach a permanent error listener later.
|
||||
wsServer.off('error', onConnectionError);
|
||||
// Unsubscribe connection error listener.
|
||||
// We'll attach a permanent error listener later.
|
||||
wsServer.off('error', onConnectionError);
|
||||
|
||||
this.listener.onListening(port);
|
||||
this.wsServer = wsServer;
|
||||
this.httpServer = server;
|
||||
this.listener.onListening(port);
|
||||
this.wsServer = wsServer;
|
||||
this.httpServer = server;
|
||||
|
||||
resolve((server.address() as AddressInfo).port);
|
||||
});
|
||||
resolve((server.address() as AddressInfo).port);
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
assertNotNull(this.wsServer);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {flipperDataFolder} from '../../utils/paths';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {getFlipperServerConfig} from '../../FlipperServerConfig';
|
||||
import {Mutex} from 'async-mutex';
|
||||
import {createSecureContext} from 'tls';
|
||||
|
||||
const tmpFile = promisify(tmp.file) as (
|
||||
options?: FileOptions,
|
||||
@@ -157,13 +158,13 @@ const certificateSetup = async () => {
|
||||
const mutex = new Mutex();
|
||||
const ensureServerCertExists = async (): Promise<void> => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const allExist = await Promise.all([
|
||||
fs.pathExists(serverKey),
|
||||
fs.pathExists(serverCert),
|
||||
fs.pathExists(caCert),
|
||||
]).then((exist) => exist.every(Boolean));
|
||||
const certs = await Promise.all([
|
||||
fs.readFile(serverKey).catch(() => ''),
|
||||
fs.readFile(serverCert).catch(() => ''),
|
||||
fs.readFile(caCert).catch(() => ''),
|
||||
]);
|
||||
|
||||
if (!allExist) {
|
||||
if (!certs.every(Boolean)) {
|
||||
console.info('No certificates were found, generating new ones');
|
||||
await generateServerCertificate();
|
||||
} else {
|
||||
@@ -172,6 +173,13 @@ const ensureServerCertExists = async (): Promise<void> => {
|
||||
await checkCertIsValid(serverCert);
|
||||
console.info('Checking certificate was issued by current CA');
|
||||
await verifyServerCertWasIssuedByCA();
|
||||
console.info('Checking certs can be used for TLS');
|
||||
// https://fb.workplace.com/groups/flippersupport/posts/1712654405881877/
|
||||
createSecureContext({
|
||||
key: certs[0],
|
||||
cert: certs[1],
|
||||
ca: certs[2],
|
||||
});
|
||||
console.info('Current certificates are valid');
|
||||
} catch (e) {
|
||||
console.warn('Not all certificates are valid, generating new ones', e);
|
||||
@@ -286,10 +294,21 @@ const getManifestPath = (config: FlipperServerConfig): string => {
|
||||
return path.resolve(config.paths.staticPath, manifestFilename);
|
||||
};
|
||||
|
||||
const exportTokenToManifest = async (
|
||||
config: FlipperServerConfig,
|
||||
token: string,
|
||||
) => {
|
||||
const exportTokenToManifest = async (token: string) => {
|
||||
console.info('Export token to manifest');
|
||||
let config: FlipperServerConfig | undefined;
|
||||
try {
|
||||
config = getFlipperServerConfig();
|
||||
} catch {
|
||||
console.warn(
|
||||
'Unable to obtain server configuration whilst exporting token to manifest',
|
||||
);
|
||||
}
|
||||
|
||||
if (!config || !config.environmentInfo.isHeadlessBuild) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifestPath = getManifestPath(config);
|
||||
try {
|
||||
const manifestData = await fs.readFile(manifestPath, {
|
||||
@@ -313,8 +332,6 @@ export const generateAuthToken = async () => {
|
||||
|
||||
await ensureServerCertExists();
|
||||
|
||||
const config = getFlipperServerConfig();
|
||||
|
||||
const privateKey = await fs.readFile(serverKey);
|
||||
const token = jwt.sign({unixname: os.userInfo().username}, privateKey, {
|
||||
algorithm: 'RS256',
|
||||
@@ -323,22 +340,44 @@ export const generateAuthToken = async () => {
|
||||
|
||||
await fs.writeFile(serverAuthToken, token);
|
||||
|
||||
console.info('Token generated and saved to disk');
|
||||
if (config.environmentInfo.isHeadlessBuild) {
|
||||
console.info('Token exported to manifest');
|
||||
await exportTokenToManifest(config, token);
|
||||
}
|
||||
await exportTokenToManifest(token);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the client authentication token. If there is no existing token,
|
||||
* it generates one, export it to the manifest file and returns it.
|
||||
*
|
||||
* Additionally, it must check the token's validity before returning it.
|
||||
* If the token is invalid, it regenerates it and exports it to the manifest file.
|
||||
*
|
||||
* Finally, the token is also exported to the manifest, on every get as to
|
||||
* ensure it is always up to date.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getAuthToken = async (): Promise<string> => {
|
||||
if (!(await hasAuthToken())) {
|
||||
return generateAuthToken();
|
||||
}
|
||||
|
||||
const token = await fs.readFile(serverAuthToken);
|
||||
return token.toString();
|
||||
const tokenBuffer = await fs.readFile(serverAuthToken);
|
||||
const token = tokenBuffer.toString();
|
||||
|
||||
try {
|
||||
console.info('Verify authentication token');
|
||||
const serverCertificate = await fs.readFile(serverCert);
|
||||
jwt.verify(token, serverCertificate);
|
||||
console.info('Token verification succeeded');
|
||||
} catch (_) {
|
||||
console.warn('Either token has expired or is invalid');
|
||||
return generateAuthToken();
|
||||
}
|
||||
|
||||
await exportTokenToManifest(token);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export const hasAuthToken = async (): Promise<boolean> => {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ERR_NO_IDB_OR_XCODE_AVAILABLE =
|
||||
export const ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB =
|
||||
'Cannot provide logs from a physical device without idb.';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
||||
type iOSSimulatorDevice = {
|
||||
state: 'Booted' | 'Shutdown' | 'Shutting Down';
|
||||
availability?: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './tracker';
|
||||
export {loadLauncherSettings} from './utils/launcherSettings';
|
||||
export {loadProcessConfig} from './utils/processConfig';
|
||||
export {getEnvironmentInfo} from './utils/environmentInfo';
|
||||
export {findInstallation} from './utils/findInstallation';
|
||||
export {getGatekeepers} from './gk';
|
||||
export {setupPrefetcher} from './fb-stubs/Prefetcher';
|
||||
export * from './server/attachSocketServer';
|
||||
|
||||
@@ -22,6 +22,7 @@ import {validateAuthToken} from '../app-connectivity/certificate-exchange/certif
|
||||
import {tracker} from '../tracker';
|
||||
import {EnvironmentInfo, isProduction} from 'flipper-common';
|
||||
import {GRAPH_SECRET} from '../fb-stubs/constants';
|
||||
import {sessionId} from '../sessionId';
|
||||
|
||||
type Config = {
|
||||
port: number;
|
||||
@@ -151,7 +152,9 @@ async function startHTTPServer(
|
||||
const processedContent = content
|
||||
.toString()
|
||||
.replace('GRAPH_SECRET_REPLACE_ME', GRAPH_SECRET)
|
||||
.replace('FLIPPER_APP_VERSION_REPLACE_ME', environmentInfo.appVersion);
|
||||
.replace('FLIPPER_APP_VERSION_REPLACE_ME', environmentInfo.appVersion)
|
||||
.replace('FLIPPER_UNIXNAME_REPLACE_ME', environmentInfo.os.unixname)
|
||||
.replace('FLIPPER_SESSION_ID_REPLACE_ME', sessionId);
|
||||
res.end(processedContent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import xdgBasedir from 'xdg-basedir';
|
||||
import net from 'net';
|
||||
import fs from 'fs-extra';
|
||||
import fetch from 'node-fetch';
|
||||
import {EnvironmentInfo} from 'flipper-common';
|
||||
import semver from 'semver';
|
||||
|
||||
@@ -39,6 +39,10 @@ export async function getEnvironmentInfo(
|
||||
process.env.FLIPPER_FORCE_VERSION ??
|
||||
(isProduction ? packageJson.version : '0.0.0');
|
||||
|
||||
if (packageJson.reactNativeOnly) {
|
||||
process.env.FLIPPER_REACT_NATIVE_ONLY = 'true';
|
||||
}
|
||||
|
||||
return {
|
||||
processId: process.pid,
|
||||
isProduction,
|
||||
|
||||
77
desktop/flipper-server-core/src/utils/findInstallation.tsx
Normal file
77
desktop/flipper-server-core/src/utils/findInstallation.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import GK from '../fb-stubs/GK';
|
||||
|
||||
const pwaRoot = path.join(
|
||||
os.homedir(),
|
||||
'Applications',
|
||||
'Chrome Apps.localized',
|
||||
);
|
||||
const appFolder = path.resolve(pwaRoot, '.flipper');
|
||||
const defaultAppPath = path.join(pwaRoot, 'Flipper.app');
|
||||
const movedAppPath = path.join(appFolder, 'Flipper.app');
|
||||
|
||||
export async function movePWA(): Promise<void> {
|
||||
if (os.platform() !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GK.get('flipper_move_pwa')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move PWA into its own folder
|
||||
// Later we will make the folder hidden so Spotlight stops indexing it
|
||||
// Sadly, Spotlight can stop indexing only hidden folder, not hidden files
|
||||
// Therefore, we have to create this parent folder in the first place.
|
||||
if (!(await fs.pathExists(appFolder))) {
|
||||
await fs.mkdir(appFolder);
|
||||
}
|
||||
await fs.move(defaultAppPath, movedAppPath);
|
||||
}
|
||||
|
||||
export async function findInstallation(): Promise<string | undefined> {
|
||||
if (os.platform() !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (GK.get('flipper_move_pwa')) {
|
||||
if (await fs.pathExists(defaultAppPath)) {
|
||||
await movePWA();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to move PWA', e);
|
||||
} finally {
|
||||
if (GK.get('flipper_move_pwa')) {
|
||||
const movedAppPlistPath = path.join(
|
||||
movedAppPath,
|
||||
'Contents',
|
||||
'Info.plist',
|
||||
);
|
||||
if (await fs.pathExists(movedAppPlistPath)) {
|
||||
return movedAppPath;
|
||||
}
|
||||
// We should get here only if moving PWA failed
|
||||
}
|
||||
const dafaultAppPlistPath = path.join(
|
||||
defaultAppPath,
|
||||
'Contents',
|
||||
'Info.plist',
|
||||
);
|
||||
if (await fs.pathExists(dafaultAppPlistPath)) {
|
||||
return defaultAppPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,13 @@ export async function runHealthcheck(
|
||||
? {
|
||||
status: 'FAILED',
|
||||
message: checkResult.message,
|
||||
commands: checkResult.commands,
|
||||
}
|
||||
: checkResult.hasProblem && !check.isRequired
|
||||
? {
|
||||
status: 'WARNING',
|
||||
message: checkResult.message,
|
||||
commands: checkResult.commands,
|
||||
}
|
||||
: {status: 'SUCCESS', message: checkResult.message};
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
|
||||
export async function findInstallation(): Promise<string | undefined> {
|
||||
if (os.platform() !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const appPath = path.join(
|
||||
os.homedir(),
|
||||
'Applications',
|
||||
'Chrome Apps.localized',
|
||||
'Flipper.app',
|
||||
);
|
||||
const appPlistPath = path.join(appPath, 'Contents', 'Info.plist');
|
||||
if (await fs.pathExists(appPlistPath)) {
|
||||
return appPath;
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
} from 'flipper-server-core';
|
||||
import {addLogTailer, isTest, LoggerFormat} from 'flipper-common';
|
||||
import exitHook from 'exit-hook';
|
||||
import {getAuthToken} from 'flipper-server-core';
|
||||
import {findInstallation} from './findInstallation';
|
||||
import {getAuthToken, findInstallation} from 'flipper-server-core';
|
||||
|
||||
const argv = yargs
|
||||
.usage('yarn flipper-server [args]')
|
||||
@@ -309,9 +308,9 @@ async function launch() {
|
||||
|
||||
const openInBrowser = async () => {
|
||||
console.info('[flipper-server] Open in browser');
|
||||
const url = new URL(`http://localhost:${argv.port}`);
|
||||
|
||||
const searchParams = new URLSearchParams({token: token ?? ''});
|
||||
const url = new URL(`http://localhost:${argv.port}?${searchParams}`);
|
||||
console.info(`[flipper-server] Go to: ${chalk.blue(url.toString())}`);
|
||||
|
||||
open(url.toString(), {app: {name: open.apps.chrome}});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
// otherwise there is an error with `declare global`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type {RenderHost} from 'flipper-ui-core';
|
||||
|
||||
declare global {
|
||||
@@ -18,6 +20,8 @@ declare global {
|
||||
};
|
||||
GRAPH_SECRET: string;
|
||||
FLIPPER_APP_VERSION: string;
|
||||
FLIPPER_SESSION_ID: string;
|
||||
FLIPPER_UNIXNAME: string;
|
||||
|
||||
flipperShowMessage?(message: string): void;
|
||||
flipperHideMessage?(): void;
|
||||
|
||||
@@ -151,6 +151,7 @@ start().catch((e) => {
|
||||
logger.track('success-rate', 'flipper-ui-browser-started', {
|
||||
value: 0,
|
||||
error: getStringFromErrorLike(e),
|
||||
pwa: window.matchMedia('(display-mode: standalone)').matches,
|
||||
});
|
||||
window.flipperShowMessage?.('Failed to start UI with error: ' + e);
|
||||
});
|
||||
|
||||
@@ -203,7 +203,7 @@ export function initializeRenderHost(
|
||||
},
|
||||
getLocalIconUrl(icon, url) {
|
||||
if (isProduction()) {
|
||||
return `icons/${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`;
|
||||
return `icons/${icon.name}-${icon.variant}_d.png`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -132,6 +132,7 @@ async function install(event: any) {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
tracker.track('pwa-install-outcome', {installed: true});
|
||||
console.log('PWA installation, user accepted the prompt.');
|
||||
return getRenderHostInstance().flipperServer.exec('move-pwa');
|
||||
} else {
|
||||
tracker.track('pwa-install-outcome', {installed: false});
|
||||
console.log('PWA installation, user dismissed the prompt.');
|
||||
|
||||
@@ -19,7 +19,7 @@ import {useCallback} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import {startPluginDownload} from '../reducers/pluginDownloads';
|
||||
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
|
||||
import {switchPlugin} from '../reducers/pluginManager';
|
||||
import {
|
||||
getActiveClient,
|
||||
getPluginDownloadStatusMap,
|
||||
|
||||
@@ -291,10 +291,11 @@ async function waitForLogin(store: Store) {
|
||||
|
||||
async function verifyFlipperIsUpToDate(title: string) {
|
||||
const serverConfig = getRenderHostInstance().serverConfig;
|
||||
// // If this is not a headless build, do not check for updates.
|
||||
// if (!serverConfig.environmentInfo.isHeadlessBuild) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// If this is not a headless build, do not check for updates.
|
||||
if (!serverConfig.environmentInfo.isHeadlessBuild) {
|
||||
return;
|
||||
}
|
||||
const config = serverConfig.processConfig;
|
||||
if (
|
||||
!isProduction() ||
|
||||
|
||||
@@ -38,7 +38,10 @@ import {
|
||||
} from '../reducers/application';
|
||||
import PluginManager from '../chrome/plugin-manager/PluginManager';
|
||||
import {showEmulatorLauncher} from './appinspect/LaunchEmulator';
|
||||
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
|
||||
import SetupDoctorScreen, {
|
||||
checkHasNewProblem,
|
||||
checkHasProblem,
|
||||
} from './SetupDoctorScreen';
|
||||
import {isProduction} from 'flipper-common';
|
||||
import FpsGraph from '../chrome/FpsGraph';
|
||||
import NetworkGraph from '../chrome/NetworkGraph';
|
||||
@@ -103,7 +106,8 @@ export const Navbar = withTrackingScope(function Navbar() {
|
||||
<TroubleshootMenu />
|
||||
<ExtrasMenu />
|
||||
<RightSidebarToggleButton />
|
||||
<UpdateIndicator />
|
||||
{getRenderHostInstance().serverConfig.environmentInfo
|
||||
.isHeadlessBuild && <UpdateIndicator />}
|
||||
</Layout.Horizontal>
|
||||
</Layout.Horizontal>
|
||||
);
|
||||
@@ -455,12 +459,32 @@ function TroubleshootMenu() {
|
||||
),
|
||||
[store, setStatus],
|
||||
);
|
||||
const [isDoctorVisible, setIsDoctorVisible] = useState(false);
|
||||
|
||||
const flipperErrorLogCount = useValue(errorCounterAtom);
|
||||
|
||||
/**
|
||||
* About Doctor. Get the healthcheck report.
|
||||
*
|
||||
* checkHasProblem: check if there are problems in the healthcheck report.
|
||||
* checkHasNewProblem: check if there are new problems in the healthcheck
|
||||
* report since the last time it was checked or acknowledged, hence the new keyword.
|
||||
*
|
||||
* The first time Flipper is opened, show doctor if there's any problems.
|
||||
* After this, use hasNewProblems as a means to show Doctor if needed.
|
||||
*/
|
||||
const result = useStore(
|
||||
(state) => state.healthchecks.healthcheckReport.result,
|
||||
);
|
||||
const hasProblem = useMemo(() => checkHasProblem(result), [result]);
|
||||
const hasNewProblem = useMemo(() => checkHasNewProblem(result), [result]);
|
||||
const flipperErrorLogCount = useValue(errorCounterAtom);
|
||||
|
||||
const [isDoctorVisible, setIsDoctorVisible] = useState(hasProblem);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNewProblem) {
|
||||
setIsDoctorVisible(true);
|
||||
}
|
||||
}, [hasNewProblem]);
|
||||
|
||||
const count = flipperErrorLogCount || hasNewProblem || 0;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Layout,
|
||||
Dialog,
|
||||
_PortalsManager,
|
||||
getFlipperLib,
|
||||
} from 'flipper-plugin';
|
||||
import {Link, styled} from '../ui';
|
||||
import {theme} from 'flipper-plugin';
|
||||
@@ -37,10 +38,11 @@ import config from '../fb-stubs/config';
|
||||
import {WelcomeScreenStaticView} from './WelcomeScreen';
|
||||
import fbConfig from '../fb-stubs/config';
|
||||
import {isFBEmployee} from '../utils/fbEmployee';
|
||||
import {notification} from 'antd';
|
||||
import {Button, Modal, notification} from 'antd';
|
||||
import isProduction from '../utils/isProduction';
|
||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||
import {uiPerfTracker} from '../utils/UIPerfTracker';
|
||||
import {WarningOutlined} from '@ant-design/icons';
|
||||
|
||||
export function SandyApp() {
|
||||
const logger = useLogger();
|
||||
@@ -48,11 +50,17 @@ export function SandyApp() {
|
||||
(state) => state.application.leftSidebarVisible,
|
||||
);
|
||||
const staticView = useStore((state) => state.connections.staticView);
|
||||
const serverConfig = getRenderHostInstance().serverConfig;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Flipper (${getVersionString()}${
|
||||
let title = `Flipper (${getVersionString()}${
|
||||
config.isFBBuild ? '@FB' : ''
|
||||
})`;
|
||||
if (!serverConfig.environmentInfo.isHeadlessBuild) {
|
||||
title += ' (Unsupported)';
|
||||
}
|
||||
|
||||
document.title = title;
|
||||
|
||||
registerStartupTime(logger);
|
||||
uiPerfTracker.track('ui-perf-sandy-container-rendered');
|
||||
@@ -73,17 +81,55 @@ export function SandyApp() {
|
||||
Dialog.showModal((onHide) => <PWAInstallationWizard onHide={onHide} />);
|
||||
}
|
||||
|
||||
showChangelog(true);
|
||||
if (serverConfig.environmentInfo.isHeadlessBuild) {
|
||||
showChangelog(true);
|
||||
}
|
||||
|
||||
// don't warn about logger, even with a new logger we don't want to re-register
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (fbConfig.warnFBEmployees && isProduction()) {
|
||||
isFBEmployee()
|
||||
.then((isEmployee) => {
|
||||
if (isEmployee) {
|
||||
isFBEmployee()
|
||||
.then((isEmployee) => {
|
||||
if (isEmployee) {
|
||||
if (process.env.FLIPPER_REACT_NATIVE_ONLY) {
|
||||
Dialog.showModal((onHide) => (
|
||||
<Modal
|
||||
closable={false}
|
||||
keyboard={false}
|
||||
maskClosable={false}
|
||||
open
|
||||
centered
|
||||
onCancel={() => onHide()}
|
||||
width={570}
|
||||
title={
|
||||
<>
|
||||
<WarningOutlined /> This Version of Flipper is Unsupported
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
getFlipperLib().openLink('munki://detail-Flipper');
|
||||
onHide();
|
||||
}}>
|
||||
Open Flipper Stable instead
|
||||
</Button>
|
||||
<Button type="ghost" onClick={() => onHide()}>
|
||||
I understand
|
||||
</Button>
|
||||
</>
|
||||
}>
|
||||
This version is only meant to be used for React Native
|
||||
debugging. It is not maintained and it doesn't receive updates.
|
||||
Instead, you should be using the main Flipper version from
|
||||
Managed Software Center for all other purposes.
|
||||
</Modal>
|
||||
));
|
||||
} else if (fbConfig.warnFBEmployees && isProduction()) {
|
||||
notification.warning({
|
||||
placement: 'bottomLeft',
|
||||
message: 'Please use Flipper@FB',
|
||||
@@ -100,11 +146,11 @@ export function SandyApp() {
|
||||
duration: null,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Failed to check if user is employee', e);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Failed to check if user is employee', e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Typography,
|
||||
Collapse,
|
||||
Button,
|
||||
List,
|
||||
Modal,
|
||||
Checkbox,
|
||||
Alert,
|
||||
@@ -70,7 +71,7 @@ const statusTypeAndMessage: {
|
||||
},
|
||||
};
|
||||
|
||||
function checkHasProblem(result: FlipperDoctor.HealthcheckResult) {
|
||||
export function checkHasProblem(result: FlipperDoctor.HealthcheckResult) {
|
||||
return result.status === 'FAILED' || result.status === 'WARNING';
|
||||
}
|
||||
|
||||
@@ -133,6 +134,25 @@ function CollapsableCategory(props: {
|
||||
header={check.label}
|
||||
extra={<CheckIcon status={check.result.status} />}>
|
||||
<Paragraph>{check.result.message}</Paragraph>
|
||||
{check.result.commands && (
|
||||
<List>
|
||||
{check.result.commands.map(({title, command}, i) => (
|
||||
<List.Item key={i}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
<Typography.Text type="secondary">{title}</Typography.Text>
|
||||
<Typography.Text code copyable>
|
||||
{command}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
@@ -274,7 +294,7 @@ export default function SetupDoctorScreen({
|
||||
return modal ? (
|
||||
<Modal
|
||||
centered
|
||||
width={570}
|
||||
width={620}
|
||||
title="Setup Doctor"
|
||||
open={visible}
|
||||
destroyOnClose
|
||||
|
||||
@@ -28,7 +28,6 @@ import {getAppVersion} from '../utils/info';
|
||||
import {getFlipperLib} from 'flipper-plugin';
|
||||
import {ReleaseChannel} from 'flipper-common';
|
||||
import {showChangelog} from '../chrome/ChangelogSheet';
|
||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||
|
||||
const RowContainer = styled(Layout.Horizontal)({
|
||||
alignItems: 'flex-start',
|
||||
@@ -143,8 +142,6 @@ export function WelcomeScreenStaticView() {
|
||||
}
|
||||
|
||||
function WelcomeScreenContent() {
|
||||
const isHeadlessBuild =
|
||||
getRenderHostInstance().serverConfig.environmentInfo.isHeadlessBuild;
|
||||
const isInsidersChannel =
|
||||
config.getReleaseChannel() === ReleaseChannel.INSIDERS;
|
||||
|
||||
@@ -160,7 +157,11 @@ function WelcomeScreenContent() {
|
||||
}}
|
||||
width={125}
|
||||
height={125}
|
||||
src={isHeadlessBuild ? './icon.png' : './icon.png'}
|
||||
src={
|
||||
process.env.FLIPPER_REACT_NATIVE_ONLY
|
||||
? './icon-rn-only.png'
|
||||
: './icon.png'
|
||||
}
|
||||
preview={false}
|
||||
/>
|
||||
<Title level={1}>Welcome to Flipper</Title>
|
||||
|
||||
@@ -32,6 +32,7 @@ import SettingsSheet from '../../chrome/SettingsSheet';
|
||||
import {Link} from '../../ui';
|
||||
import {chain, uniq, without} from 'lodash';
|
||||
import {ReactNode} from 'react-markdown';
|
||||
import {produce} from 'immer';
|
||||
|
||||
const COLD_BOOT = 'cold-boot';
|
||||
|
||||
@@ -108,6 +109,8 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
||||
setFavoriteVirtualDevices(without(favoriteVirtualDevices, deviceName));
|
||||
};
|
||||
|
||||
const [pendingEmulators, setPendingEmulators] = useState(new Set<string>());
|
||||
|
||||
useEffect(() => {
|
||||
const getiOSSimulators = async () => {
|
||||
if (!iosEnabled) {
|
||||
@@ -157,102 +160,140 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
||||
return <NoSDKsEnabledAlert onClose={onClose} />;
|
||||
}
|
||||
|
||||
const items = [
|
||||
<Title key="android-title" name="Android emulators" />,
|
||||
androidEmulators.length == 0 ? (
|
||||
<Typography.Paragraph style={{textAlign: 'center'}}>
|
||||
{androidMessage}
|
||||
</Typography.Paragraph>
|
||||
) : null,
|
||||
...chain(
|
||||
androidEmulators.map((name) => ({
|
||||
name,
|
||||
isFavorite: favoriteVirtualDevices.includes(name),
|
||||
})),
|
||||
)
|
||||
.sortBy((item) => [!item.isFavorite, item.name])
|
||||
.map(({name, isFavorite}) => {
|
||||
const launch = (coldBoot: boolean) => {
|
||||
getRenderHostInstance()
|
||||
.flipperServer.exec('android-launch-emulator', name, coldBoot)
|
||||
.then(onClose)
|
||||
.catch((e) => {
|
||||
let items: (JSX.Element | null)[] = [];
|
||||
if (androidEnabled) {
|
||||
items.push(
|
||||
<Title key="android-title" name="Android emulators" />,
|
||||
androidEmulators.length == 0 ? (
|
||||
<Typography.Paragraph style={{textAlign: 'center'}}>
|
||||
{androidMessage}
|
||||
</Typography.Paragraph>
|
||||
) : null,
|
||||
...chain(
|
||||
androidEmulators.map((name) => ({
|
||||
name,
|
||||
isFavorite: favoriteVirtualDevices.includes(name),
|
||||
})),
|
||||
)
|
||||
.sortBy((item) => [!item.isFavorite, item.name])
|
||||
.map(({name, isFavorite}) => {
|
||||
const launch = async (coldBoot: boolean) => {
|
||||
try {
|
||||
setPendingEmulators(
|
||||
produce((draft) => {
|
||||
draft.add(name);
|
||||
}),
|
||||
);
|
||||
await getRenderHostInstance().flipperServer.exec(
|
||||
'android-launch-emulator',
|
||||
name,
|
||||
coldBoot,
|
||||
);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.error('Failed to start emulator: ', e);
|
||||
message.error('Failed to start emulator: ' + e);
|
||||
});
|
||||
};
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={({key}) => {
|
||||
switch (key) {
|
||||
case COLD_BOOT: {
|
||||
launch(true);
|
||||
break;
|
||||
} finally {
|
||||
setPendingEmulators(
|
||||
produce((draft) => {
|
||||
draft.delete(name);
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={({key}) => {
|
||||
switch (key) {
|
||||
case COLD_BOOT: {
|
||||
launch(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Menu.Item key={COLD_BOOT} icon={<PoweroffOutlined />}>
|
||||
Cold Boot
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
}}>
|
||||
<Menu.Item key={COLD_BOOT} icon={<PoweroffOutlined />}>
|
||||
Cold Boot
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<VirtualDeviceRow
|
||||
key={name}
|
||||
addToFavorites={addToFavorites}
|
||||
removeFromFavorites={removeFromFavorites}
|
||||
isFavorite={isFavorite}
|
||||
name={name}>
|
||||
<Dropdown.Button
|
||||
overlay={menu}
|
||||
icon={<MoreOutlined />}
|
||||
loading={pendingEmulators.has(name)}
|
||||
onClick={() => launch(false)}>
|
||||
{name}
|
||||
</Dropdown.Button>
|
||||
</VirtualDeviceRow>
|
||||
);
|
||||
})
|
||||
.value(),
|
||||
);
|
||||
}
|
||||
if (iosEnabled) {
|
||||
items.push(
|
||||
<Title key="ios-title" name="iOS Simulators" />,
|
||||
iosEmulators.length == 0 ? (
|
||||
<Typography.Paragraph style={{textAlign: 'center'}}>
|
||||
{iOSMessage}
|
||||
</Typography.Paragraph>
|
||||
) : null,
|
||||
...chain(iosEmulators)
|
||||
.map((device) => ({
|
||||
device,
|
||||
isFavorite: favoriteVirtualDevices.includes(device.name),
|
||||
}))
|
||||
.sortBy((item) => [!item.isFavorite, item.device.name])
|
||||
.map(({device, isFavorite}) => (
|
||||
<VirtualDeviceRow
|
||||
key={name}
|
||||
key={device.udid}
|
||||
addToFavorites={addToFavorites}
|
||||
removeFromFavorites={removeFromFavorites}
|
||||
isFavorite={isFavorite}
|
||||
name={name}>
|
||||
<Dropdown.Button
|
||||
overlay={menu}
|
||||
icon={<MoreOutlined />}
|
||||
onClick={() => launch(false)}>
|
||||
{name}
|
||||
</Dropdown.Button>
|
||||
</VirtualDeviceRow>
|
||||
);
|
||||
})
|
||||
.value(),
|
||||
|
||||
<Title key="ios-title" name="iOS Simulators" />,
|
||||
iosEmulators.length == 0 ? (
|
||||
<Typography.Paragraph style={{textAlign: 'center'}}>
|
||||
{iOSMessage}
|
||||
</Typography.Paragraph>
|
||||
) : null,
|
||||
...chain(iosEmulators)
|
||||
.map((device) => ({
|
||||
device,
|
||||
isFavorite: favoriteVirtualDevices.includes(device.name),
|
||||
}))
|
||||
.sortBy((item) => [!item.isFavorite, item.device.name])
|
||||
.map(({device, isFavorite}) => (
|
||||
<VirtualDeviceRow
|
||||
key={device.udid}
|
||||
addToFavorites={addToFavorites}
|
||||
removeFromFavorites={removeFromFavorites}
|
||||
isFavorite={isFavorite}
|
||||
name={device.name}>
|
||||
<Button
|
||||
type="default"
|
||||
key={device.udid}
|
||||
style={{width: '100%'}}
|
||||
onClick={() =>
|
||||
getRenderHostInstance()
|
||||
.flipperServer.exec('ios-launch-simulator', device.udid)
|
||||
.catch((e) => {
|
||||
name={device.name}>
|
||||
<Button
|
||||
type="default"
|
||||
key={device.udid}
|
||||
style={{width: '100%'}}
|
||||
loading={pendingEmulators.has(device.udid)}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setPendingEmulators(
|
||||
produce((draft) => {
|
||||
draft.add(device.udid);
|
||||
}),
|
||||
);
|
||||
await getRenderHostInstance().flipperServer.exec(
|
||||
'ios-launch-simulator',
|
||||
device.udid,
|
||||
);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.warn('Failed to start simulator: ', e);
|
||||
message.error('Failed to start simulator: ' + e);
|
||||
})
|
||||
.then(onClose)
|
||||
}>
|
||||
{device.name}
|
||||
{device.osVersion ? ` (${device.osVersion})` : ''}
|
||||
</Button>
|
||||
</VirtualDeviceRow>
|
||||
))
|
||||
.value(),
|
||||
].filter((item) => item != null);
|
||||
} finally {
|
||||
setPendingEmulators(
|
||||
produce((draft) => {
|
||||
draft.delete(device.udid);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}>
|
||||
{device.name}
|
||||
{device.osVersion ? ` (${device.osVersion})` : ''}
|
||||
</Button>
|
||||
</VirtualDeviceRow>
|
||||
))
|
||||
.value(),
|
||||
);
|
||||
}
|
||||
items = items.filter((item) => item != null);
|
||||
|
||||
const loadingSpinner = (
|
||||
<>
|
||||
|
||||
@@ -162,7 +162,11 @@ function init(flipperServer: FlipperServer) {
|
||||
// We could potentially merge ui-perf-store-rehydrated and ui-perf-everything-finally-loaded-jeeeez,
|
||||
// but what if at some point in the future we relalize that store rehydration is not actually the last event?
|
||||
// Keep it separate for the time being (evil laugh as there is nothing more permanent than temporary stuff)
|
||||
uiPerfTracker.track('ui-perf-everything-finally-loaded-jeeeez');
|
||||
uiPerfTracker.track('ui-perf-everything-finally-loaded-jeeeez', {
|
||||
numberOfPlugins:
|
||||
store.getState().plugins.clientPlugins.size +
|
||||
store.getState().plugins.devicePlugins.size,
|
||||
});
|
||||
});
|
||||
|
||||
setPersistor(persistor);
|
||||
|
||||
@@ -11,7 +11,7 @@ import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {getIconURL} from '../../utils/icons';
|
||||
|
||||
export type IconSize = 8 | 10 | 12 | 16 | 18 | 20 | 24 | 28 | 32;
|
||||
export type IconSize = 8 | 10 | 12 | 16 | 18 | 20 | 24 | 28 | 32 | 48;
|
||||
|
||||
const ColoredIconBlack = styled.img<{size: number}>(({size}) => ({
|
||||
height: size,
|
||||
@@ -94,7 +94,7 @@ function ColoredIcon(
|
||||
}
|
||||
ColoredIcon.displayName = 'Glyph:ColoredIcon';
|
||||
|
||||
export default class Glyph extends React.PureComponent<{
|
||||
export default function Glyph(props: {
|
||||
name: string;
|
||||
size?: IconSize;
|
||||
variant?: 'filled' | 'outline';
|
||||
@@ -102,33 +102,22 @@ export default class Glyph extends React.PureComponent<{
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
title?: string;
|
||||
}> {
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
size = 16,
|
||||
variant,
|
||||
color,
|
||||
className,
|
||||
style,
|
||||
title,
|
||||
} = this.props;
|
||||
}) {
|
||||
const {name, size = 16, variant, color, className, style, title} = props;
|
||||
|
||||
return (
|
||||
<ColoredIcon
|
||||
name={name}
|
||||
className={className}
|
||||
color={color}
|
||||
size={size}
|
||||
title={title}
|
||||
src={getIconURL({
|
||||
name,
|
||||
variant: variant ?? 'filled',
|
||||
size,
|
||||
density: typeof window !== 'undefined' ? window.devicePixelRatio : 1,
|
||||
})}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ColoredIcon
|
||||
name={name}
|
||||
className={className}
|
||||
color={color}
|
||||
size={size}
|
||||
title={title}
|
||||
src={getIconURL({
|
||||
name,
|
||||
variant: variant ?? 'filled',
|
||||
size,
|
||||
})}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ class UIPerfTracker {
|
||||
this.t0 = performance.now();
|
||||
}
|
||||
|
||||
track(event: UIPerfEvents) {
|
||||
track(event: UIPerfEvents, data?: any) {
|
||||
const tx = performance.now();
|
||||
getLogger().track('performance', event, {
|
||||
time: tx - this.t0,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,17 @@
|
||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||
import {IconSize} from '../ui/components/Glyph';
|
||||
|
||||
const AVAILABLE_SIZES: IconSize[] = [8, 10, 12, 16, 18, 20, 24, 28, 32];
|
||||
const DENSITIES = [1, 1.5, 2, 3, 4];
|
||||
const AVAILABLE_SIZES: IconSize[] = [8, 10, 12, 16, 18, 20, 24, 28, 32, 48];
|
||||
|
||||
export type Icon = {
|
||||
name: string;
|
||||
variant: 'outline' | 'filled';
|
||||
size: IconSize;
|
||||
density: number;
|
||||
};
|
||||
|
||||
function normalizeIcon(icon: Icon): Icon {
|
||||
let {size, density} = icon;
|
||||
let requestedSize = size as number;
|
||||
if (!AVAILABLE_SIZES.includes(size as any)) {
|
||||
let requestedSize = icon.size as number;
|
||||
if (!AVAILABLE_SIZES.includes(icon.size as any)) {
|
||||
// find the next largest size
|
||||
const possibleSize = AVAILABLE_SIZES.find((size) => {
|
||||
return size > requestedSize;
|
||||
@@ -37,28 +34,13 @@ function normalizeIcon(icon: Icon): Icon {
|
||||
}
|
||||
}
|
||||
|
||||
if (!DENSITIES.includes(density)) {
|
||||
// find the next largest size
|
||||
const possibleDensity = DENSITIES.find((scale) => {
|
||||
return scale > density;
|
||||
});
|
||||
|
||||
// set to largest size if the real size is larger than what we have
|
||||
if (possibleDensity == null) {
|
||||
density = Math.max(...DENSITIES);
|
||||
} else {
|
||||
density = possibleDensity;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...icon,
|
||||
size: requestedSize as IconSize,
|
||||
density,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPublicIconUrl({name, variant, size, density}: Icon) {
|
||||
export function getPublicIconUrl({name, variant, size}: Icon) {
|
||||
return `https://facebook.com/images/assets_DO_NOT_HARDCODE/facebook_icons/${name}_${variant}_${size}.png`;
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
"npm": "use yarn instead",
|
||||
"yarn": "^1.16"
|
||||
},
|
||||
"version": "0.227.0",
|
||||
"version": "0.233.0",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"scripts",
|
||||
|
||||
@@ -27,7 +27,7 @@ You also need to compile in the `litho-annotations` package, as Flipper reflects
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-litho-plugin:0.227.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-litho-plugin:0.233.0'
|
||||
debugImplementation 'com.facebook.litho:litho-annotations:0.19.0'
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ To setup the <Link to={useBaseUrl("/docs/features/plugins/leak-canary")}>LeakCan
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-leakcanary2-plugin:0.227.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-leakcanary2-plugin:0.233.0'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ The network plugin is shipped as a separate Maven artifact, as follows:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.227.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.233.0'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Events = {
|
||||
init: InitEvent;
|
||||
subtreeUpdate: SubtreeUpdateEvent;
|
||||
frameScan: FrameScanEvent;
|
||||
traversalError: TraversalErrorEvent;
|
||||
perfStats: PerfStatsEvent;
|
||||
performanceStats: PerformanceStatsEvent;
|
||||
metadataUpdate: UpdateMetadataEvent;
|
||||
@@ -34,6 +35,13 @@ export type FrameScanEvent = {
|
||||
frameworkEvents?: FrameworkEvent[];
|
||||
};
|
||||
|
||||
export type TraversalErrorEvent = {
|
||||
nodeName: String;
|
||||
errorType: String;
|
||||
errorMessage: String;
|
||||
stack: String;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated This event should not be used and soon will
|
||||
* be removed. FrameScan should be used instead.
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ClientNode,
|
||||
Metadata,
|
||||
SnapshotInfo,
|
||||
MetadataId,
|
||||
} from './ClientTypes';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
|
||||
@@ -26,6 +27,7 @@ export type LiveClientState = {
|
||||
nodes: Map<Id, ClientNode>;
|
||||
};
|
||||
|
||||
export type MetadataMap = Map<MetadataId, Metadata>;
|
||||
export type Color = string;
|
||||
|
||||
export type UIState = {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {useHotkeys} from 'react-hotkeys-hook';
|
||||
import {Id, Metadata, MetadataId, ClientNode} from '../ClientTypes';
|
||||
import {PerfStats} from './PerfStats';
|
||||
import {Visualization2D} from './visualizer/Visualization2D';
|
||||
import {Inspector} from './sidebar/Inspector';
|
||||
import {TreeControls} from './tree/TreeControls';
|
||||
import {Button, Spin, Typography} from 'antd';
|
||||
import {QueryClientProvider} from 'react-query';
|
||||
@@ -30,6 +29,8 @@ import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
|
||||
import {queryClient} from '../utils/reactQuery';
|
||||
import {FrameworkEventsTable} from './FrameworkEventsTable';
|
||||
import {Centered} from './shared/Centered';
|
||||
import {SidebarV2} from './sidebarV2/SidebarV2';
|
||||
import {getNode} from '../utils/map';
|
||||
|
||||
export function Component() {
|
||||
const instance = usePlugin(plugin);
|
||||
@@ -38,6 +39,7 @@ export function Component() {
|
||||
const visualiserWidth = useValue(instance.uiState.visualiserWidth);
|
||||
const nodes: Map<Id, ClientNode> = useValue(instance.nodes);
|
||||
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
|
||||
const selectedNodeId = useValue(instance.uiState.selectedNode);
|
||||
|
||||
const [showPerfStats, setShowPerfStats] = useState(false);
|
||||
|
||||
@@ -155,11 +157,10 @@ export function Component() {
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<DetailSidebar width={450}>
|
||||
<Inspector
|
||||
os={instance.os}
|
||||
<SidebarV2
|
||||
metadata={metadata}
|
||||
nodes={nodes}
|
||||
showExtra={openBottomPanelWithContent}
|
||||
selectedNode={getNode(selectedNodeId?.id, nodes)}
|
||||
showBottomPanel={openBottomPanelWithContent}
|
||||
/>
|
||||
</DetailSidebar>
|
||||
</Layout.Horizontal>
|
||||
|
||||
@@ -126,7 +126,7 @@ export const Inspector: React.FC<Props> = ({
|
||||
frameworkEventMetadata={frameworkEventMetadata}
|
||||
node={selectedNode}
|
||||
events={selectedFrameworkEvents}
|
||||
showExtra={showExtra}
|
||||
showBottomPanel={showExtra}
|
||||
/>
|
||||
</Tab>
|
||||
)}
|
||||
|
||||
@@ -27,13 +27,6 @@ type Props = {
|
||||
color: Color;
|
||||
};
|
||||
|
||||
const DefaultColor: Color = {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 1,
|
||||
};
|
||||
|
||||
const CenteredContentContainer = styled.div(AutoMarginStyle);
|
||||
const ObjectContainer = styled.div(ObjectContainerStyle);
|
||||
const NumberValue = styled.span(NumberAttributeValueStyle);
|
||||
|
||||
@@ -42,7 +42,7 @@ import {tracker} from '../../../utils/tracker';
|
||||
type Props = {
|
||||
node: ClientNode;
|
||||
events: readonly FrameworkEvent[];
|
||||
showExtra?: (title: string, element: ReactNode) => void;
|
||||
showBottomPanel?: (title: string, element: ReactNode) => void;
|
||||
frameworkEventMetadata: Map<FrameworkEventType, FrameworkEventMetadata>;
|
||||
onSetViewMode: (viewMode: ViewMode) => void;
|
||||
};
|
||||
@@ -50,7 +50,7 @@ type Props = {
|
||||
export const FrameworkEventsInspector: React.FC<Props> = ({
|
||||
node,
|
||||
events,
|
||||
showExtra,
|
||||
showBottomPanel: showExtra,
|
||||
frameworkEventMetadata,
|
||||
onSetViewMode,
|
||||
}) => {
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Button, Divider, Input, Modal, Typography} from 'antd';
|
||||
|
||||
import {
|
||||
DataInspector,
|
||||
Panel,
|
||||
theme,
|
||||
Layout,
|
||||
styled,
|
||||
useLocalStorageState,
|
||||
} from 'flipper-plugin';
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
ClientNode,
|
||||
Color,
|
||||
Inspectable,
|
||||
InspectableObject,
|
||||
Metadata,
|
||||
} from '../../ClientTypes';
|
||||
import {MetadataMap} from '../../DesktopTypes';
|
||||
import {NoData} from '../sidebar/inspector/NoData';
|
||||
import {css, cx} from '@emotion/css';
|
||||
import {upperFirst, sortBy, omit} from 'lodash';
|
||||
import {any} from 'lodash/fp';
|
||||
import {InspectableColor} from '../../ClientTypes';
|
||||
import {transformAny} from '../../utils/dataTransform';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
|
||||
type ModalData = {
|
||||
data: unknown;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const panelCss = css`
|
||||
& > .ant-collapse-item .ant-collapse-header {
|
||||
background-color: ${theme.backgroundDefault};
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
& > .ant-collapse-item .ant-collapse-header .ant-collapse-expand-icon {
|
||||
width: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function AttributesInspector({
|
||||
node,
|
||||
metadata,
|
||||
}: {
|
||||
node: ClientNode;
|
||||
metadata: MetadataMap;
|
||||
}) {
|
||||
const [modalData, setModalData] = useState<ModalData | null>(null);
|
||||
|
||||
const [attributeFilter, setAttributeFilter] = useLocalStorageState(
|
||||
'attribute-filter',
|
||||
'',
|
||||
);
|
||||
|
||||
const showComplexTypeModal = (modaldata: ModalData) => {
|
||||
setModalData(modaldata);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setModalData(null);
|
||||
};
|
||||
|
||||
const keys = Object.keys(node.attributes);
|
||||
const sections = keys
|
||||
.map((key, _) => {
|
||||
/**
|
||||
* The node top-level attributes refer to the displayable panels aka sections.
|
||||
* The panel name is obtained by querying the metadata.
|
||||
* The inspectable contains the actual attributes belonging to each panel.
|
||||
*/
|
||||
const metadataId: number = Number(key);
|
||||
const sectionMetadata = metadata.get(metadataId);
|
||||
if (sectionMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
const sectionAttributes = node.attributes[
|
||||
metadataId
|
||||
] as InspectableObject;
|
||||
|
||||
return AttributeSection(
|
||||
metadata,
|
||||
sectionMetadata.name,
|
||||
sectionAttributes,
|
||||
showComplexTypeModal,
|
||||
attributeFilter,
|
||||
);
|
||||
})
|
||||
.filter((section) => section != null);
|
||||
|
||||
if (sections.length === 0 && !attributeFilter) {
|
||||
return <NoData message="No data available for this element" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalData != null && (
|
||||
<Modal
|
||||
title={modalData.title}
|
||||
open
|
||||
onOk={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
footer={null}>
|
||||
<DataInspector data={modalData.data} />
|
||||
</Modal>
|
||||
)}
|
||||
<Layout.Container gap="small" padv="medium">
|
||||
<Input
|
||||
value={attributeFilter}
|
||||
onChange={(e) => setAttributeFilter(e.target.value)}
|
||||
placeholder="Filter attributes"
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
|
||||
{sections.length === 0 ? (
|
||||
<NoData message="No attributes match filter " />
|
||||
) : (
|
||||
sections.concat([
|
||||
<Panel key="Raw" title="Raw Data" className={panelCss} collapsed>
|
||||
<DataInspector data={omit(node, ['attributes'])} />
|
||||
</Panel>,
|
||||
])
|
||||
)}
|
||||
</Layout.Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AttributeSection(
|
||||
metadataMap: MetadataMap,
|
||||
name: string,
|
||||
inspectable: InspectableObject,
|
||||
onDisplayModal: (modaldata: ModalData) => void,
|
||||
attributeFilter: string,
|
||||
) {
|
||||
const attributesOrSubSubsections = Object.entries(inspectable.fields)
|
||||
.map(([fieldKey, attributeValue]) => {
|
||||
const metadataId: number = Number(fieldKey);
|
||||
const attributeMetadata = metadataMap.get(metadataId);
|
||||
const attributeName =
|
||||
upperFirst(attributeMetadata?.name) ?? String(metadataId);
|
||||
//subsections are complex types that are only 1 level deep
|
||||
const isSubSection =
|
||||
attributeValue.type === 'object' &&
|
||||
!any(
|
||||
(inspectable) =>
|
||||
inspectable.type === 'array' || inspectable.type === 'object',
|
||||
Object.values(attributeValue.fields),
|
||||
);
|
||||
return {
|
||||
attributeName,
|
||||
attributeMetadata,
|
||||
isSubSection,
|
||||
attributeValue,
|
||||
metadataId,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
({attributeName}) =>
|
||||
!attributeFilter ||
|
||||
attributeName.toLowerCase().includes(attributeFilter),
|
||||
);
|
||||
|
||||
//push sub sections to the end
|
||||
const sortedAttributesOrSubsections = sortBy(
|
||||
attributesOrSubSubsections,
|
||||
[(item) => item.isSubSection],
|
||||
(item) => item.attributeName,
|
||||
);
|
||||
|
||||
const children = sortedAttributesOrSubsections
|
||||
.map(({isSubSection, attributeValue, attributeMetadata, attributeName}) => {
|
||||
if (attributeMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSubSection) {
|
||||
if (attributeValue.type === 'object') {
|
||||
return (
|
||||
<SubSection
|
||||
onDisplayModal={onDisplayModal}
|
||||
attributeName={attributeName}
|
||||
inspectableObject={attributeValue}
|
||||
metadataMap={metadataMap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<NamedAttribute
|
||||
attributeMetadata={attributeMetadata}
|
||||
onDisplayModal={onDisplayModal}
|
||||
key={attributeName}
|
||||
metadataMap={metadataMap}
|
||||
name={attributeName}
|
||||
value={attributeValue}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter((attr) => attr != null);
|
||||
|
||||
if (children.length > 0) {
|
||||
return (
|
||||
<Panel className={panelCss} key={name} title={name}>
|
||||
<Layout.Container gap="small" padv="small" style={{paddingLeft: 18}}>
|
||||
{...children}
|
||||
</Layout.Container>
|
||||
</Panel>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function SubSection({
|
||||
attributeName,
|
||||
inspectableObject,
|
||||
metadataMap,
|
||||
onDisplayModal,
|
||||
}: {
|
||||
attributeName: string;
|
||||
inspectableObject: InspectableObject;
|
||||
metadataMap: MetadataMap;
|
||||
onDisplayModal: (modaldata: ModalData) => void;
|
||||
}) {
|
||||
const children = Object.entries(inspectableObject.fields).map(
|
||||
([key, value]) => {
|
||||
const metadataId: number = Number(key);
|
||||
const attributeMetadata = metadataMap.get(metadataId);
|
||||
if (attributeMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
const attributeName =
|
||||
upperFirst(attributeMetadata?.name) ?? String(metadataId);
|
||||
|
||||
return (
|
||||
<NamedAttribute
|
||||
key={key}
|
||||
onDisplayModal={onDisplayModal}
|
||||
name={attributeName}
|
||||
value={value}
|
||||
attributeMetadata={attributeMetadata}
|
||||
metadataMap={metadataMap}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Layout.Container gap="small" padv="small">
|
||||
<Divider style={{margin: 0}} />
|
||||
<Typography.Text>{attributeName}</Typography.Text>
|
||||
{children}
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
|
||||
function NamedAttribute({
|
||||
key,
|
||||
name,
|
||||
value,
|
||||
metadataMap,
|
||||
attributeMetadata,
|
||||
onDisplayModal,
|
||||
}: {
|
||||
name: string;
|
||||
value: Inspectable;
|
||||
attributeMetadata: Metadata;
|
||||
metadataMap: MetadataMap;
|
||||
key: string;
|
||||
onDisplayModal: (modaldata: ModalData) => void;
|
||||
}) {
|
||||
return (
|
||||
<Layout.Horizontal key={key} gap="small">
|
||||
<Typography.Text
|
||||
style={{
|
||||
marginTop: 4, //to center with top input when multiline
|
||||
flex: '0 0 40%', //take 40% of the width
|
||||
color: theme.textColorSecondary,
|
||||
opacity: 0.7,
|
||||
fontWeight: 50,
|
||||
fontSize: 'small',
|
||||
}}>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
|
||||
<Layout.Container style={{flex: '1 1 auto'}}>
|
||||
<AttributeValue
|
||||
onDisplayModal={onDisplayModal}
|
||||
name={name}
|
||||
attributeMetadata={attributeMetadata}
|
||||
metadataMap={metadataMap}
|
||||
inspectable={value}
|
||||
/>
|
||||
</Layout.Container>
|
||||
</Layout.Horizontal>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* disables hover and focsued states
|
||||
*/
|
||||
const readOnlyInput = css`
|
||||
font-size: small;
|
||||
:hover {
|
||||
border-color: ${theme.disabledColor} !important;
|
||||
}
|
||||
:focus {
|
||||
border-color: ${theme.disabledColor} !important;
|
||||
|
||||
box-shadow: none !important;
|
||||
}
|
||||
box-shadow: none !important;
|
||||
border-color: ${theme.disabledColor} !important;
|
||||
|
||||
padding: 2px 4px 2px 4px;
|
||||
|
||||
min-height: 20px !important; //this is for text area
|
||||
`;
|
||||
|
||||
function StyledInput({
|
||||
value,
|
||||
color,
|
||||
mutable,
|
||||
rightAddon,
|
||||
}: {
|
||||
value: any;
|
||||
color: string;
|
||||
mutable: boolean;
|
||||
rightAddon?: string;
|
||||
}) {
|
||||
let formatted: any = value;
|
||||
if (typeof value === 'number') {
|
||||
//cap the number of decimal places to 5 but dont add trailing zeros
|
||||
formatted = Number.parseFloat(value.toFixed(5));
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
size="small"
|
||||
className={cx(
|
||||
!mutable ? readOnlyInput : '',
|
||||
css`
|
||||
//set input colour when no suffix
|
||||
color: ${color};
|
||||
//set input colour when has suffix
|
||||
.ant-input.ant-input-sm[type='text'] {
|
||||
color: ${color};
|
||||
}
|
||||
//set colour of suffix
|
||||
.ant-input.ant-input-sm[type='text'] + .ant-input-suffix {
|
||||
color: ${theme.textColorSecondary};
|
||||
opacity: 0.7;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
bordered
|
||||
readOnly={!mutable}
|
||||
value={formatted}
|
||||
suffix={rightAddon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function StyledTextArea({
|
||||
value,
|
||||
color,
|
||||
mutable,
|
||||
}: {
|
||||
value: any;
|
||||
color: string;
|
||||
mutable: boolean;
|
||||
rightAddon?: string;
|
||||
}) {
|
||||
return (
|
||||
<Input.TextArea
|
||||
autoSize
|
||||
className={!mutable ? readOnlyInput : ''}
|
||||
bordered
|
||||
style={{color: color}}
|
||||
readOnly={!mutable}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const boolColor = '#C41D7F';
|
||||
const stringColor = '#AF5800';
|
||||
const enumColor = '#006D75';
|
||||
const numberColor = '#003EB3';
|
||||
|
||||
type NumberGroupValue = {value: number; addonText: string};
|
||||
|
||||
function NumberGroup({values}: {values: NumberGroupValue[]}) {
|
||||
return (
|
||||
<Layout.Horizontal gap="small">
|
||||
{values.map(({value, addonText}, idx) => (
|
||||
<StyledInput
|
||||
key={idx}
|
||||
color={numberColor}
|
||||
mutable={false}
|
||||
value={value}
|
||||
rightAddon={addonText}
|
||||
/>
|
||||
))}
|
||||
</Layout.Horizontal>
|
||||
);
|
||||
}
|
||||
|
||||
function AttributeValue({
|
||||
metadataMap,
|
||||
name,
|
||||
onDisplayModal,
|
||||
inspectable,
|
||||
}: {
|
||||
onDisplayModal: (modaldata: ModalData) => void;
|
||||
attributeMetadata: Metadata;
|
||||
metadataMap: MetadataMap;
|
||||
name: string;
|
||||
inspectable: Inspectable;
|
||||
}) {
|
||||
switch (inspectable.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<StyledInput
|
||||
color={boolColor}
|
||||
mutable={false}
|
||||
value={inspectable.value ? 'TRUE' : 'FALSE'}
|
||||
/>
|
||||
);
|
||||
case 'unknown':
|
||||
case 'text':
|
||||
return (
|
||||
<StyledTextArea
|
||||
color={stringColor}
|
||||
mutable={false}
|
||||
value={inspectable.value}
|
||||
/>
|
||||
);
|
||||
case 'number':
|
||||
return (
|
||||
<StyledInput
|
||||
color={numberColor}
|
||||
mutable={false}
|
||||
value={inspectable.value}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'enum':
|
||||
return (
|
||||
<StyledInput
|
||||
color={enumColor}
|
||||
mutable={false}
|
||||
value={inspectable.value}
|
||||
/>
|
||||
);
|
||||
case 'size':
|
||||
return (
|
||||
<NumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.width, addonText: 'W'},
|
||||
{value: inspectable.value.height, addonText: 'H'},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'coordinate':
|
||||
return (
|
||||
<NumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.x, addonText: 'X'},
|
||||
{value: inspectable.value.y, addonText: 'Y'},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case 'coordinate3d':
|
||||
return (
|
||||
<NumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.x, addonText: 'X'},
|
||||
{value: inspectable.value.y, addonText: 'Y'},
|
||||
{value: inspectable.value.z, addonText: 'Z'},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case 'space':
|
||||
return (
|
||||
<TwoByTwoNumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.top, addonText: 'T'},
|
||||
{value: inspectable.value.left, addonText: 'L'},
|
||||
{value: inspectable.value.bottom, addonText: 'B'},
|
||||
{value: inspectable.value.right, addonText: 'R'},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case 'bounds':
|
||||
return (
|
||||
<TwoByTwoNumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.x, addonText: 'X'},
|
||||
{value: inspectable.value.y, addonText: 'Y'},
|
||||
{value: inspectable.value.width, addonText: 'W'},
|
||||
{value: inspectable.value.height, addonText: 'H'},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'color':
|
||||
return <ColorInspector inspectable={inspectable as InspectableColor} />;
|
||||
case 'array':
|
||||
case 'object':
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onDisplayModal({
|
||||
title: name,
|
||||
data: transformAny(metadataMap, inspectable),
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
height: 26,
|
||||
boxSizing: 'border-box',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
type="ghost">
|
||||
<span
|
||||
style={{
|
||||
marginTop: 2,
|
||||
fontFamily: 'monospace',
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 'small',
|
||||
}}>
|
||||
{inspectable.type === 'array' ? '[...]' : '{...}'}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowHeight = 26;
|
||||
|
||||
function ColorInspector({inspectable}: {inspectable: InspectableColor}) {
|
||||
return (
|
||||
<Layout.Container gap="small">
|
||||
<NumberGroup
|
||||
values={[
|
||||
{value: inspectable.value.r, addonText: 'R'},
|
||||
{value: inspectable.value.g, addonText: 'G'},
|
||||
{value: inspectable.value.b, addonText: 'B'},
|
||||
{value: inspectable.value.a, addonText: 'A'},
|
||||
]}
|
||||
/>
|
||||
<Layout.Horizontal gap="medium">
|
||||
<ColorPreview
|
||||
background={`rgba(${inspectable.value.r},${inspectable.value.g},${inspectable.value.b},${inspectable.value.a})`}
|
||||
/>
|
||||
<StyledTextArea
|
||||
color={stringColor}
|
||||
mutable={false}
|
||||
value={RGBAtoHEX(inspectable.value)}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
|
||||
const ColorPreview = styled.div(({background}: {background: string}) => ({
|
||||
width: rowHeight,
|
||||
height: rowHeight,
|
||||
borderRadius: '8px',
|
||||
borderColor: theme.disabledColor,
|
||||
borderStyle: 'solid',
|
||||
boxSizing: 'border-box',
|
||||
borderWidth: '1px',
|
||||
backgroundColor: background,
|
||||
}));
|
||||
|
||||
const RGBAtoHEX = (color: Color) => {
|
||||
const hex =
|
||||
(color.r | (1 << 8)).toString(16).slice(1) +
|
||||
(color.g | (1 << 8)).toString(16).slice(1) +
|
||||
(color.b | (1 << 8)).toString(16).slice(1);
|
||||
|
||||
return '#' + hex.toUpperCase();
|
||||
};
|
||||
|
||||
type FourItemArray<T = any> = [T, T, T, T];
|
||||
|
||||
function TwoByTwoNumberGroup({
|
||||
values,
|
||||
}: {
|
||||
values: FourItemArray<NumberGroupValue>;
|
||||
}) {
|
||||
return (
|
||||
<Layout.Container gap="small" style={{flex: '0 1 auto'}}>
|
||||
<NumberGroup values={[values[0], values[1]]} />
|
||||
<NumberGroup values={[values[2], values[3]]} />
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {ClientNode, MetadataId, Metadata} from '../../ClientTypes';
|
||||
import {plugin} from '../../index';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {Layout, Tab, Tabs, usePlugin, useValue} from 'flipper-plugin';
|
||||
import {NoData} from '../sidebar/inspector/NoData';
|
||||
import {Tooltip} from 'antd';
|
||||
import {AttributesInspector} from './AttributesInspector';
|
||||
import {FrameworkEventsInspector} from '../sidebar/inspector/FrameworkEventsInspector';
|
||||
|
||||
type Props = {
|
||||
selectedNode?: ClientNode;
|
||||
metadata: Map<MetadataId, Metadata>;
|
||||
showBottomPanel: (title: string, element: ReactNode) => void;
|
||||
};
|
||||
export function SidebarV2({selectedNode, metadata, showBottomPanel}: Props) {
|
||||
const instance = usePlugin(plugin);
|
||||
|
||||
const frameworkEventMetadata = useValue(instance.frameworkEventMetadata);
|
||||
|
||||
if (!selectedNode) {
|
||||
return <NoData message="Please select a node to view its details" />;
|
||||
}
|
||||
const selectedFrameworkEvents = selectedNode.id
|
||||
? instance.frameworkEvents.getAllRecordsByIndex({nodeId: selectedNode.id})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Layout.Container gap pad>
|
||||
<Tabs
|
||||
localStorageKeyOverride="sidebar-tabs"
|
||||
grow
|
||||
centered
|
||||
key={selectedNode.id}>
|
||||
<Tab tab={<Tooltip title="Attributes">Attributes</Tooltip>}>
|
||||
<AttributesInspector node={selectedNode} metadata={metadata} />
|
||||
</Tab>
|
||||
{selectedFrameworkEvents?.length > 0 && (
|
||||
<Tab
|
||||
key={'events'}
|
||||
tab={
|
||||
<Tooltip title="Events">
|
||||
<Layout.Horizontal center>Events</Layout.Horizontal>
|
||||
</Tooltip>
|
||||
}>
|
||||
<FrameworkEventsInspector
|
||||
onSetViewMode={instance.uiActions.onSetViewMode}
|
||||
frameworkEventMetadata={frameworkEventMetadata}
|
||||
node={selectedNode}
|
||||
events={selectedFrameworkEvents}
|
||||
showBottomPanel={showBottomPanel}
|
||||
/>
|
||||
</Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import {checkFocusedNodeStillActive} from './plugin/ClientDataUtils';
|
||||
import {uiActions} from './plugin/uiActions';
|
||||
import {first} from 'lodash';
|
||||
import {getNode} from './utils/map';
|
||||
import {handleTraversalError} from './plugin/traversalError';
|
||||
|
||||
export function plugin(client: PluginClient<Events, Methods>) {
|
||||
const rootId = createState<Id | undefined>(undefined);
|
||||
@@ -114,6 +115,8 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
||||
});
|
||||
});
|
||||
|
||||
handleTraversalError(client);
|
||||
|
||||
client.onConnect(() => {
|
||||
uiState.isConnected.set(true);
|
||||
console.log('[ui-debugger] connected');
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "*",
|
||||
"@emotion/styled": "*",
|
||||
"@emotion/css": "*",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
|
||||
29
desktop/plugins/public/ui-debugger/plugin/traversalError.tsx
Normal file
29
desktop/plugins/public/ui-debugger/plugin/traversalError.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {notification} from 'antd';
|
||||
import {PluginClient} from 'flipper-plugin';
|
||||
import {Events, Methods} from '../ClientTypes';
|
||||
|
||||
export function handleTraversalError(client: PluginClient<Events, Methods>) {
|
||||
client.onMessage('traversalError', (event) => {
|
||||
notification.warn({
|
||||
key: 'client-traversal-error',
|
||||
duration: 60,
|
||||
message: 'Error fetching UI dump',
|
||||
description: `There was an error UI dump, ${event.errorType} ${event.errorMessage}. We are aware of this and looking into it. Please try again later.`,
|
||||
});
|
||||
console.error(
|
||||
`[ui-debugger] Client error during traversal: `,
|
||||
event,
|
||||
client.appName,
|
||||
client.device.os,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
MetadataId,
|
||||
} from '../ClientTypes';
|
||||
|
||||
function transformAny(
|
||||
export function transformAny(
|
||||
metadata: Map<MetadataId, Metadata>,
|
||||
inspectable: Inspectable,
|
||||
): any {
|
||||
@@ -32,6 +32,8 @@ function transformAny(
|
||||
case 'enum':
|
||||
case 'space':
|
||||
return inspectable.value;
|
||||
case 'array':
|
||||
return inspectable.items.map((value) => transformAny(metadata, value));
|
||||
case 'object':
|
||||
return transformObject(metadata, inspectable);
|
||||
default:
|
||||
|
||||
@@ -132,7 +132,12 @@ const argv = yargs
|
||||
default: false,
|
||||
},
|
||||
mac: {
|
||||
describe: 'Build a platform-specific bundle for MacOS.',
|
||||
describe: 'Build arm64 and x64 bundles for MacOS.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
'mac-local': {
|
||||
describe: 'Build local architecture bundle for MacOS.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
@@ -432,7 +437,7 @@ async function buildServerRelease() {
|
||||
await fs.mkdirp(path.join(dir, 'static', 'defaultPlugins'));
|
||||
|
||||
await prepareDefaultPlugins(argv.channel === 'insiders');
|
||||
await compileServerMain(false);
|
||||
await compileServerMain();
|
||||
await copyStaticResources(dir, versionNumber);
|
||||
await linkLocalDeps(dir);
|
||||
await downloadIcons(path.join(dir, 'static'));
|
||||
@@ -455,6 +460,15 @@ async function buildServerRelease() {
|
||||
platforms.push(BuildPlatform.MAC_X64);
|
||||
platforms.push(BuildPlatform.MAC_AARCH64);
|
||||
}
|
||||
if (argv.macLocal) {
|
||||
const architecture = os.arch();
|
||||
console.log(`⚙️ Local architecture: ${architecture}`);
|
||||
if (architecture == 'arm64') {
|
||||
platforms.push(BuildPlatform.MAC_AARCH64);
|
||||
} else {
|
||||
platforms.push(BuildPlatform.MAC_X64);
|
||||
}
|
||||
}
|
||||
if (argv.win) {
|
||||
platforms.push(BuildPlatform.WINDOWS);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import fetch from '@adobe/node-fetch-retry';
|
||||
// eslint-disable-next-line node/no-extraneous-import
|
||||
import type {Icon} from 'flipper-ui-core';
|
||||
|
||||
const AVAILABLE_SIZES: Icon['size'][] = [8, 10, 12, 16, 18, 20, 24, 28, 32];
|
||||
const AVAILABLE_SIZES: Icon['size'][] = [8, 10, 12, 16, 18, 20, 24, 28, 32, 48];
|
||||
|
||||
export type Icons = {
|
||||
[key: string]: Icon['size'][];
|
||||
@@ -32,37 +32,26 @@ function getIconPartsFromName(icon: string): {
|
||||
}
|
||||
|
||||
export async function downloadIcons(buildFolder: string) {
|
||||
const icons: Icons = JSON.parse(
|
||||
const icons: string[] = JSON.parse(
|
||||
await fs.promises.readFile(path.join(buildFolder, 'icons.json'), {
|
||||
encoding: 'utf8',
|
||||
}),
|
||||
);
|
||||
const iconURLs = Object.entries(icons).reduce<Icon[]>(
|
||||
(acc, [entryName, sizes]) => {
|
||||
const {trimmedName: name, variant} = getIconPartsFromName(entryName);
|
||||
acc.push(
|
||||
// get icons in @1x and @2x
|
||||
...sizes.map((size) => ({name, variant, size, density: 1})),
|
||||
...sizes.map((size) => ({name, variant, size, density: 2})),
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const iconURLs: Pick<Icon, 'name' | 'variant'>[] = icons.map((rawName) => {
|
||||
const {trimmedName: name, variant} = getIconPartsFromName(rawName);
|
||||
return {name, variant};
|
||||
});
|
||||
|
||||
// Download first largest instance of each icon
|
||||
await Promise.all(
|
||||
iconURLs.map(async (icon) => {
|
||||
const sizeIndex = AVAILABLE_SIZES.indexOf(icon.size);
|
||||
if (sizeIndex === -1) {
|
||||
throw new Error('Size unavailable: ' + icon.size);
|
||||
}
|
||||
const sizesToTry = AVAILABLE_SIZES.slice(sizeIndex);
|
||||
const sizesToTry = [...AVAILABLE_SIZES];
|
||||
|
||||
while (sizesToTry.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const size = sizesToTry.shift()!;
|
||||
const size = sizesToTry.pop()!;
|
||||
|
||||
const url = getPublicIconUrl({...icon, size});
|
||||
const url = getPublicIconUrl({...(icon as Icon), size});
|
||||
const res = await fetch(url);
|
||||
if (res.status !== 200) {
|
||||
// console.log(
|
||||
@@ -88,21 +77,21 @@ export async function downloadIcons(buildFolder: string) {
|
||||
console.error(
|
||||
`Could not download the icon ${JSON.stringify(
|
||||
icon,
|
||||
)} from ${getPublicIconUrl(icon)}, didn't find any matching size`,
|
||||
)} from ${getPublicIconUrl({
|
||||
...icon,
|
||||
size: AVAILABLE_SIZES[AVAILABLE_SIZES.length - 1],
|
||||
} as Icon)}, didn't find any matching size`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// should match flipper-ui-core/src/utils/icons.tsx
|
||||
export function getPublicIconUrl({name, variant, size, density}: Icon) {
|
||||
export function getPublicIconUrl({name, variant, size}: Icon) {
|
||||
return `https://facebook.com/images/assets_DO_NOT_HARDCODE/facebook_icons/${name}_${variant}_${size}.png`;
|
||||
}
|
||||
|
||||
// should match app/src/utils/icons.tsx
|
||||
function buildLocalIconPath(icon: Icon) {
|
||||
return path.join(
|
||||
'icons',
|
||||
`${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`,
|
||||
);
|
||||
function buildLocalIconPath(icon: Pick<Icon, 'name' | 'variant'>) {
|
||||
return path.join('icons', `${icon.name}-${icon.variant}_d.png`);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ const argv = yargs
|
||||
'Unique build identifier to be used as the version patch part for the build',
|
||||
type: 'number',
|
||||
},
|
||||
'react-native-only': {
|
||||
description: 'React Native only build',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
channel: {
|
||||
description: 'Release channel for the build',
|
||||
choices: ['stable', 'insiders'],
|
||||
@@ -115,6 +120,10 @@ if (argv['default-plugins-dir']) {
|
||||
process.env.FLIPPER_DEFAULT_PLUGINS_DIR = argv['default-plugins-dir'];
|
||||
}
|
||||
|
||||
if (argv['react-native-only']) {
|
||||
process.env.FLIPPER_REACT_NATIVE_ONLY = 'true';
|
||||
}
|
||||
|
||||
async function generateManifest(versionNumber: string) {
|
||||
await fs.writeFile(
|
||||
path.join(distDir, 'manifest.json'),
|
||||
@@ -130,6 +139,7 @@ async function modifyPackageManifest(
|
||||
versionNumber: string,
|
||||
hgRevision: string | null,
|
||||
channel: string,
|
||||
reactNativeOnly: boolean,
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Creating package.json manifest');
|
||||
@@ -148,6 +158,7 @@ async function modifyPackageManifest(
|
||||
manifest.revision = hgRevision;
|
||||
}
|
||||
manifest.releaseChannel = channel;
|
||||
manifest.reactNativeOnly = reactNativeOnly;
|
||||
await fs.writeFile(
|
||||
path.join(buildFolder, 'package.json'),
|
||||
JSON.stringify(manifest, null, ' '),
|
||||
@@ -265,6 +276,9 @@ async function buildDist(buildFolder: string) {
|
||||
},
|
||||
mac: {
|
||||
bundleVersion: FIX_RELEASE_VERSION,
|
||||
icon: process.env.FLIPPER_REACT_NATIVE_ONLY
|
||||
? path.resolve(buildFolder, 'icon-rn-only.icns')
|
||||
: path.resolve(buildFolder, 'icon.icns'),
|
||||
},
|
||||
win: {
|
||||
signAndEditExecutable: !isFB,
|
||||
@@ -299,7 +313,13 @@ async function copyStaticFolder(buildFolder: string) {
|
||||
await moveSourceMaps(dir, argv['source-map-dir']);
|
||||
const versionNumber = getVersionNumber(argv.version);
|
||||
const hgRevision = await genMercurialRevision();
|
||||
await modifyPackageManifest(dir, versionNumber, hgRevision, argv.channel);
|
||||
await modifyPackageManifest(
|
||||
dir,
|
||||
versionNumber,
|
||||
hgRevision,
|
||||
argv.channel,
|
||||
argv['react-native-only'],
|
||||
);
|
||||
await fs.ensureDir(distDir);
|
||||
await generateManifest(versionNumber);
|
||||
await buildDist(dir);
|
||||
|
||||
@@ -279,7 +279,7 @@ export function genMercurialRevision(): Promise<string | null> {
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
export async function compileServerMain(dev: boolean) {
|
||||
export async function compileServerMain() {
|
||||
console.log('⚙️ Compiling server sources...');
|
||||
await exec(`cd ${serverDir} && yarn build`);
|
||||
console.log('✅ Compiled server sources.');
|
||||
|
||||
@@ -70,6 +70,10 @@ const argv = yargs
|
||||
'[FB-internal only] Will yield `true` on any GK. Disabled by default. Setting env var FLIPPER_ENABLE_ALL_GKS is equivalent',
|
||||
type: 'boolean',
|
||||
},
|
||||
'react-native-only': {
|
||||
description: '[FB-internal only] React Native only build',
|
||||
type: 'boolean',
|
||||
},
|
||||
channel: {
|
||||
describe:
|
||||
'[FB-internal only] Release channel. "stable" by default. Setting env var "FLIPPER_RELEASE_CHANNEL" is equivalent.',
|
||||
@@ -148,6 +152,10 @@ if (argv.channel !== undefined) {
|
||||
process.env.FLIPPER_RELEASE_CHANNEL = argv.channel;
|
||||
}
|
||||
|
||||
if (argv['react-native-only'] === true) {
|
||||
process.env.FLIPPER_REACT_NATIVE_ONLY = 'true';
|
||||
}
|
||||
|
||||
if (argv['force-version']) {
|
||||
process.env.FLIPPER_FORCE_VERSION = argv['force-version'];
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ async function copyStaticResources() {
|
||||
|
||||
async function restartServer() {
|
||||
try {
|
||||
await compileServerMain(true);
|
||||
await compileServerMain();
|
||||
await launchServer(true, ++startCount === 1); // only open on the first time
|
||||
} catch (e) {
|
||||
console.error(
|
||||
|
||||
@@ -16,7 +16,6 @@ import {EOL} from 'os';
|
||||
import pmap from 'p-map';
|
||||
import {rootDir} from './paths';
|
||||
import yargs from 'yargs';
|
||||
import {isPluginJson} from 'flipper-common';
|
||||
|
||||
const argv = yargs
|
||||
.usage('yarn tsc-plugins [args]')
|
||||
|
||||
@@ -1,680 +1,197 @@
|
||||
{
|
||||
"accessibility": [
|
||||
16,
|
||||
20
|
||||
],
|
||||
"app-dailies": [
|
||||
12
|
||||
],
|
||||
"app-react": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"apps": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"arrow-right": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"bell-null-outline": [
|
||||
24
|
||||
],
|
||||
"bell-null": [
|
||||
12
|
||||
],
|
||||
"bell": [
|
||||
12
|
||||
],
|
||||
"bird": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"borders": [
|
||||
16
|
||||
],
|
||||
"box": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"brush-paint": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"bug": [
|
||||
12,
|
||||
20
|
||||
],
|
||||
"building-city": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"camcorder": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"camera": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"caution-octagon": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"caution-triangle": [
|
||||
12,
|
||||
16,
|
||||
20,
|
||||
24
|
||||
],
|
||||
"caution": [
|
||||
16,
|
||||
24
|
||||
],
|
||||
"checkmark": [
|
||||
16
|
||||
],
|
||||
"chevron-down-outline": [
|
||||
10
|
||||
],
|
||||
"chevron-down": [
|
||||
12,
|
||||
16,
|
||||
8
|
||||
],
|
||||
"chevron-left": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"chevron-right": [
|
||||
8,
|
||||
12,
|
||||
16
|
||||
],
|
||||
"chevron-up": [
|
||||
12,
|
||||
16,
|
||||
8
|
||||
],
|
||||
"compose": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"copy": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"cross-circle": [
|
||||
12,
|
||||
16,
|
||||
24
|
||||
],
|
||||
"cross": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"dashboard-outline": [
|
||||
24
|
||||
],
|
||||
"dashboard": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"data-table": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"desktop": [
|
||||
12
|
||||
],
|
||||
"directions": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"dots-3-circle-outline": [
|
||||
16
|
||||
],
|
||||
"download": [
|
||||
16
|
||||
],
|
||||
"face-unhappy-outline": [
|
||||
24
|
||||
],
|
||||
"first-aid": [
|
||||
12
|
||||
],
|
||||
"flash-default": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"info-circle": [
|
||||
12,
|
||||
16,
|
||||
24
|
||||
],
|
||||
"internet": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"life-event-major": [
|
||||
16
|
||||
],
|
||||
"magic-wand": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"magnifying-glass": [
|
||||
16,
|
||||
20,
|
||||
24
|
||||
],
|
||||
"messages": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"minus-circle": [
|
||||
12
|
||||
],
|
||||
"mobile-engagement": [
|
||||
16
|
||||
],
|
||||
"mobile": [
|
||||
12,
|
||||
16,
|
||||
32
|
||||
],
|
||||
"network": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"news-feed": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"pause": [
|
||||
16
|
||||
],
|
||||
"posts": [
|
||||
20
|
||||
],
|
||||
"power": [
|
||||
16
|
||||
],
|
||||
"profile": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"question-circle-outline": [
|
||||
16
|
||||
],
|
||||
"question-circle": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"question": [
|
||||
16
|
||||
],
|
||||
"refresh-left": [
|
||||
16
|
||||
],
|
||||
"rocket": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"settings": [
|
||||
12
|
||||
],
|
||||
"share-external": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"share": [
|
||||
16
|
||||
],
|
||||
"star-outline": [
|
||||
12,
|
||||
16,
|
||||
24
|
||||
],
|
||||
"star-slash": [
|
||||
16
|
||||
],
|
||||
"star": [
|
||||
12,
|
||||
16,
|
||||
24
|
||||
],
|
||||
"stop-playback": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"stop": [
|
||||
16,
|
||||
24
|
||||
],
|
||||
"target": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"thought-bubble": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"tools": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"translate": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"trash-outline": [
|
||||
16
|
||||
],
|
||||
"trash": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"tree": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"trending": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"triangle-down": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"triangle-right": [
|
||||
12
|
||||
],
|
||||
"underline": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"washing-machine": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"watch-tv": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"gears-two": [
|
||||
16
|
||||
],
|
||||
"info-cursive": [
|
||||
16
|
||||
],
|
||||
"on-this-day": [
|
||||
12
|
||||
],
|
||||
"zoom-out": [
|
||||
16
|
||||
],
|
||||
"zoom-in": [
|
||||
16
|
||||
],
|
||||
"fast-forward": [
|
||||
16
|
||||
],
|
||||
"draft-outline": [
|
||||
16
|
||||
],
|
||||
"gradient": [
|
||||
16
|
||||
],
|
||||
"crop": [
|
||||
16
|
||||
],
|
||||
"play": [
|
||||
16
|
||||
],
|
||||
"cross-outline": [
|
||||
16
|
||||
],
|
||||
"messenger-code": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"book": [
|
||||
12
|
||||
],
|
||||
"list-arrow-up": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"cat": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"duplicate": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"profile-circle-outline": [
|
||||
16
|
||||
],
|
||||
"card-person": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"pencil-outline": [
|
||||
16
|
||||
],
|
||||
"code": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"undo-outline": [
|
||||
16
|
||||
],
|
||||
"checkmark-circle-outline": [
|
||||
24
|
||||
],
|
||||
"target-outline": [
|
||||
16,
|
||||
24
|
||||
],
|
||||
"internet-outline": [
|
||||
24,
|
||||
32
|
||||
],
|
||||
"profile-outline": [
|
||||
32
|
||||
],
|
||||
"app-react-outline": [
|
||||
16
|
||||
],
|
||||
"send-outline": [
|
||||
16
|
||||
],
|
||||
"paper-stack": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"weather-cold": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"mobile-cross": [
|
||||
16
|
||||
],
|
||||
"database-arrow-left": [
|
||||
12
|
||||
],
|
||||
"plus-circle-outline": [
|
||||
16
|
||||
],
|
||||
"arrows-circle": [
|
||||
12
|
||||
],
|
||||
"navicon": [
|
||||
12
|
||||
],
|
||||
"paper-fold-text": [
|
||||
16
|
||||
],
|
||||
"marketplace": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"workflow": [
|
||||
12
|
||||
],
|
||||
"sankey-diagram": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"media-stack": [
|
||||
16
|
||||
],
|
||||
"question-hexagon": [
|
||||
16
|
||||
],
|
||||
"briefcase": [
|
||||
16
|
||||
],
|
||||
"business-briefcase": [
|
||||
16
|
||||
],
|
||||
"log": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"triangle-up": [
|
||||
16,
|
||||
20
|
||||
],
|
||||
"checkmark-circle": [
|
||||
12,
|
||||
20
|
||||
],
|
||||
"circle": [
|
||||
12
|
||||
],
|
||||
"comment-swish": [
|
||||
16
|
||||
],
|
||||
"direct": [
|
||||
16
|
||||
],
|
||||
"plus": [
|
||||
16
|
||||
],
|
||||
"scuba": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"line-chart": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"caution-circle": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"megaphone": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"wireless": [
|
||||
16
|
||||
],
|
||||
"cup-outline": [
|
||||
24
|
||||
],
|
||||
"unicorn": [
|
||||
20
|
||||
],
|
||||
"turtle": [
|
||||
20
|
||||
],
|
||||
"sushi": [
|
||||
12
|
||||
],
|
||||
"arrows-up-down": [
|
||||
16
|
||||
],
|
||||
"style-effects": [
|
||||
16
|
||||
],
|
||||
"stopwatch": [
|
||||
16
|
||||
],
|
||||
"database": [
|
||||
16
|
||||
],
|
||||
"bar-chart": [
|
||||
16
|
||||
],
|
||||
"augmented-reality": [
|
||||
16
|
||||
],
|
||||
"app-flash": [
|
||||
16
|
||||
],
|
||||
"sample-lo": [
|
||||
20
|
||||
],
|
||||
"point": [
|
||||
20
|
||||
],
|
||||
"eye": [
|
||||
16
|
||||
],
|
||||
"send": [
|
||||
16
|
||||
],
|
||||
"refresh-right": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"hourglass": [
|
||||
16
|
||||
],
|
||||
"mobile-outline": [
|
||||
16,
|
||||
24
|
||||
],
|
||||
"bookmark-outline": [
|
||||
16
|
||||
],
|
||||
"app-facebook-f-outline": [
|
||||
24
|
||||
],
|
||||
"app-messenger-outline": [
|
||||
24
|
||||
],
|
||||
"app-instagram-outline": [
|
||||
24
|
||||
],
|
||||
"app-whatsapp-outline": [
|
||||
24
|
||||
],
|
||||
"app-workplace-outline": [
|
||||
24
|
||||
],
|
||||
"app-work-chat-outline": [
|
||||
24
|
||||
],
|
||||
"sample-hi": [
|
||||
20
|
||||
],
|
||||
"dog": [
|
||||
12,
|
||||
16
|
||||
],
|
||||
"hub": [
|
||||
16
|
||||
],
|
||||
"upload": [
|
||||
16
|
||||
],
|
||||
"list-gear-outline": [
|
||||
24
|
||||
],
|
||||
"app-apple-outline": [
|
||||
24
|
||||
],
|
||||
"app-microsoft-windows": [
|
||||
24
|
||||
],
|
||||
"box-outline": [
|
||||
24
|
||||
],
|
||||
"differential": [
|
||||
16
|
||||
],
|
||||
"raincloud": [
|
||||
16
|
||||
],
|
||||
"app-microsoft-windows-outline": [
|
||||
24
|
||||
],
|
||||
"camcorder-live": [
|
||||
16
|
||||
],
|
||||
"plumbing": [
|
||||
16
|
||||
],
|
||||
"app-facebook-circle": [
|
||||
16
|
||||
],
|
||||
"link": [
|
||||
16
|
||||
],
|
||||
"commercial-break-usd": [
|
||||
16
|
||||
],
|
||||
"friends-engagement": [
|
||||
16
|
||||
],
|
||||
"app-cms": [
|
||||
16
|
||||
],
|
||||
"caution-triangle-outline": [
|
||||
24
|
||||
],
|
||||
"bird-flying": [
|
||||
16
|
||||
],
|
||||
"arrows-left-right": [
|
||||
16
|
||||
],
|
||||
"grid-9": [
|
||||
16
|
||||
],
|
||||
"stethoscope": [
|
||||
16
|
||||
],
|
||||
"friend-except": [
|
||||
16
|
||||
],
|
||||
"app-instagram": [
|
||||
16
|
||||
],
|
||||
"nav-magnifying-glass": [
|
||||
16
|
||||
],
|
||||
"list-arrow-down": [
|
||||
16
|
||||
],
|
||||
"photo-arrows-left-right": [
|
||||
16
|
||||
],
|
||||
"badge": [
|
||||
16
|
||||
],
|
||||
"square-ruler": [
|
||||
16
|
||||
],
|
||||
"phone": [
|
||||
16
|
||||
],
|
||||
"app-horizon-assets": [
|
||||
16
|
||||
],
|
||||
"app-bloks": [
|
||||
16
|
||||
],
|
||||
"settings-internal": [
|
||||
16
|
||||
],
|
||||
"weather-thunder-outline": [
|
||||
16
|
||||
],
|
||||
"weather-thunder": [
|
||||
16
|
||||
]
|
||||
}
|
||||
[
|
||||
"accessibility",
|
||||
"app-dailies",
|
||||
"app-react",
|
||||
"apps",
|
||||
"arrow-right",
|
||||
"bell-null-outline",
|
||||
"bell-null",
|
||||
"bell",
|
||||
"bird",
|
||||
"borders",
|
||||
"box",
|
||||
"brush-paint",
|
||||
"bug",
|
||||
"building-city",
|
||||
"camcorder",
|
||||
"camera",
|
||||
"caution-octagon",
|
||||
"caution-triangle",
|
||||
"caution",
|
||||
"checkmark",
|
||||
"chevron-down-outline",
|
||||
"chevron-down",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"chevron-up",
|
||||
"compose",
|
||||
"copy",
|
||||
"cross-circle",
|
||||
"cross",
|
||||
"dashboard-outline",
|
||||
"dashboard",
|
||||
"data-table",
|
||||
"desktop",
|
||||
"directions",
|
||||
"dots-3-circle-outline",
|
||||
"download",
|
||||
"face-unhappy-outline",
|
||||
"first-aid",
|
||||
"flash-default",
|
||||
"info-circle",
|
||||
"internet",
|
||||
"life-event-major",
|
||||
"magic-wand",
|
||||
"magnifying-glass",
|
||||
"messages",
|
||||
"minus-circle",
|
||||
"mobile-engagement",
|
||||
"mobile",
|
||||
"network",
|
||||
"news-feed",
|
||||
"pause",
|
||||
"posts",
|
||||
"power",
|
||||
"profile",
|
||||
"question-circle-outline",
|
||||
"question-circle",
|
||||
"question",
|
||||
"refresh-left",
|
||||
"rocket",
|
||||
"settings",
|
||||
"share-external",
|
||||
"share",
|
||||
"star-outline",
|
||||
"star-slash",
|
||||
"star",
|
||||
"stop-playback",
|
||||
"stop",
|
||||
"target",
|
||||
"thought-bubble",
|
||||
"tools",
|
||||
"translate",
|
||||
"trash-outline",
|
||||
"trash",
|
||||
"tree",
|
||||
"trending",
|
||||
"triangle-down",
|
||||
"triangle-right",
|
||||
"underline",
|
||||
"washing-machine",
|
||||
"watch-tv",
|
||||
"gears-two",
|
||||
"info-cursive",
|
||||
"on-this-day",
|
||||
"zoom-out",
|
||||
"zoom-in",
|
||||
"fast-forward",
|
||||
"draft-outline",
|
||||
"gradient",
|
||||
"crop",
|
||||
"play",
|
||||
"cross-outline",
|
||||
"messenger-code",
|
||||
"book",
|
||||
"list-arrow-up",
|
||||
"cat",
|
||||
"duplicate",
|
||||
"profile-circle-outline",
|
||||
"card-person",
|
||||
"pencil-outline",
|
||||
"code",
|
||||
"undo-outline",
|
||||
"checkmark-circle-outline",
|
||||
"target-outline",
|
||||
"internet-outline",
|
||||
"profile-outline",
|
||||
"app-react-outline",
|
||||
"send-outline",
|
||||
"paper-stack",
|
||||
"weather-cold",
|
||||
"mobile-cross",
|
||||
"database-arrow-left",
|
||||
"plus-circle-outline",
|
||||
"arrows-circle",
|
||||
"navicon",
|
||||
"paper-fold-text",
|
||||
"marketplace",
|
||||
"workflow",
|
||||
"sankey-diagram",
|
||||
"media-stack",
|
||||
"question-hexagon",
|
||||
"briefcase",
|
||||
"business-briefcase",
|
||||
"log",
|
||||
"triangle-up",
|
||||
"checkmark-circle",
|
||||
"circle",
|
||||
"comment-swish",
|
||||
"direct",
|
||||
"plus",
|
||||
"scuba",
|
||||
"line-chart",
|
||||
"caution-circle",
|
||||
"megaphone",
|
||||
"wireless",
|
||||
"cup-outline",
|
||||
"unicorn",
|
||||
"turtle",
|
||||
"sushi",
|
||||
"arrows-up-down",
|
||||
"style-effects",
|
||||
"stopwatch",
|
||||
"database",
|
||||
"bar-chart",
|
||||
"augmented-reality",
|
||||
"app-flash",
|
||||
"sample-lo",
|
||||
"point",
|
||||
"eye",
|
||||
"send",
|
||||
"refresh-right",
|
||||
"hourglass",
|
||||
"mobile-outline",
|
||||
"bookmark-outline",
|
||||
"app-facebook-f-outline",
|
||||
"app-messenger-outline",
|
||||
"app-instagram-outline",
|
||||
"app-whatsapp-outline",
|
||||
"app-workplace-outline",
|
||||
"app-work-chat-outline",
|
||||
"sample-hi",
|
||||
"dog",
|
||||
"hub",
|
||||
"upload",
|
||||
"list-gear-outline",
|
||||
"app-apple-outline",
|
||||
"app-microsoft-windows",
|
||||
"box-outline",
|
||||
"differential",
|
||||
"raincloud",
|
||||
"app-microsoft-windows-outline",
|
||||
"camcorder-live",
|
||||
"plumbing",
|
||||
"app-facebook-circle",
|
||||
"link",
|
||||
"commercial-break-usd",
|
||||
"friends-engagement",
|
||||
"app-cms",
|
||||
"caution-triangle-outline",
|
||||
"bird-flying",
|
||||
"arrows-left-right",
|
||||
"grid-9",
|
||||
"stethoscope",
|
||||
"friend-except",
|
||||
"app-instagram",
|
||||
"nav-magnifying-glass",
|
||||
"list-arrow-down",
|
||||
"photo-arrows-left-right",
|
||||
"badge",
|
||||
"square-ruler",
|
||||
"phone",
|
||||
"app-horizon-assets",
|
||||
"app-bloks",
|
||||
"settings-internal",
|
||||
"weather-thunder-outline",
|
||||
"weather-thunder"
|
||||
]
|
||||
@@ -139,6 +139,8 @@
|
||||
|
||||
window.GRAPH_SECRET = 'GRAPH_SECRET_REPLACE_ME';
|
||||
window.FLIPPER_APP_VERSION = 'FLIPPER_APP_VERSION_REPLACE_ME';
|
||||
window.FLIPPER_SESSION_ID = 'FLIPPER_SESSION_ID_REPLACE_ME';
|
||||
window.FLIPPER_UNIXNAME = 'FLIPPER_UNIXNAME_REPLACE_ME';
|
||||
|
||||
const params = new URL(location.href).searchParams;
|
||||
let token = params.get('token');
|
||||
@@ -199,9 +201,19 @@
|
||||
function init() {
|
||||
const script = document.createElement('script');
|
||||
script.src = window.flipperConfig.entryPoint;
|
||||
|
||||
script.onerror = (e) => {
|
||||
showMessage('Failed to load entry point. Check Chrome Dev Tools console for more info.');
|
||||
const retry = (retries) => {
|
||||
showMessage(`Failed to load entry point. Check Chrome Dev Tools console for more info. Retrying in: ${retries}`);
|
||||
retries -= 1;
|
||||
if (retries < 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
setTimeout(() => retry(retries), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
retry(3);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
|
||||
window.GRAPH_SECRET = 'GRAPH_SECRET_REPLACE_ME';
|
||||
window.FLIPPER_APP_VERSION = 'FLIPPER_APP_VERSION_REPLACE_ME';
|
||||
window.FLIPPER_SESSION_ID = 'FLIPPER_SESSION_ID_REPLACE_ME';
|
||||
window.FLIPPER_UNIXNAME = 'FLIPPER_UNIXNAME_REPLACE_ME';
|
||||
|
||||
// load correct theme (n.b. this doesn't handle system value specifically, will assume light in such cases)
|
||||
try {
|
||||
@@ -104,9 +106,19 @@
|
||||
function init() {
|
||||
const script = document.createElement('script');
|
||||
script.src = window.flipperConfig.entryPoint;
|
||||
|
||||
script.onerror = (e) => {
|
||||
showMessage('Script failure. Check Chrome Dev Tools console for more info.');
|
||||
const retry = (retries) => {
|
||||
showMessage(`Failed to load entry point. Check Chrome Dev Tools console for more info. Retrying in: ${retries}`);
|
||||
retries -= 1;
|
||||
if (retries < 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
setTimeout(() => retry(retries), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
retry(3);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
@@ -28,7 +28,6 @@ import fixPath from 'fix-path';
|
||||
import {exec} from 'child_process';
|
||||
import setup, {Config, configPath} from './setup';
|
||||
import isFB from './fb-stubs/isFB';
|
||||
import delegateToLauncher from './launcher';
|
||||
import yargs from 'yargs';
|
||||
import {promisify} from 'util';
|
||||
import process from 'process';
|
||||
@@ -174,58 +173,50 @@ app.on('ready', async () => {
|
||||
const config = await setup(argv);
|
||||
processConfig(config);
|
||||
|
||||
// If we delegate to the launcher, shut down this instance of the app.
|
||||
delegateToLauncher(argv)
|
||||
.then(async (hasLauncherInvoked: boolean) => {
|
||||
if (hasLauncherInvoked) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
appReady = true;
|
||||
app.commandLine.appendSwitch('scroll-bounce');
|
||||
configureSession();
|
||||
createWindow(config);
|
||||
appReady = true;
|
||||
|
||||
// if in development install the react devtools extension
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const {
|
||||
default: installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} = require('electron-devtools-installer');
|
||||
// if set, try to download a newever version of the dev tools
|
||||
const forceDownload = process.env.FLIPPER_UPDATE_DEV_TOOLS === 'true';
|
||||
if (forceDownload) {
|
||||
console.log('Force updating DevTools');
|
||||
}
|
||||
// React
|
||||
// Fix for extension loading (see D27685981)
|
||||
// Work around per https://github.com/electron/electron/issues/23662#issuecomment-787420799
|
||||
const reactDevToolsPath = `${os.homedir()}/Library/Application Support/Electron/extensions/${
|
||||
REACT_DEVELOPER_TOOLS.id
|
||||
}`;
|
||||
if (await promisify(fs.exists)(reactDevToolsPath)) {
|
||||
console.log('Loading React devtools from disk ' + reactDevToolsPath);
|
||||
try {
|
||||
await session.defaultSession.loadExtension(
|
||||
reactDevToolsPath,
|
||||
// @ts-ignore only supported (and needed) in Electron 12
|
||||
{allowFileAccess: true},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to load React devtools from disk: ', e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await installExtension(REACT_DEVELOPER_TOOLS.id, {
|
||||
loadExtensionOptions: {allowFileAccess: true, forceDownload},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to install React devtools extension', e);
|
||||
}
|
||||
}
|
||||
app.commandLine.appendSwitch('scroll-bounce');
|
||||
configureSession();
|
||||
createWindow(config);
|
||||
|
||||
// if in development install the react devtools extension
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const {
|
||||
default: installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} = require('electron-devtools-installer');
|
||||
// if set, try to download a newever version of the dev tools
|
||||
const forceDownload = process.env.FLIPPER_UPDATE_DEV_TOOLS === 'true';
|
||||
if (forceDownload) {
|
||||
console.log('Force updating DevTools');
|
||||
}
|
||||
// React
|
||||
// Fix for extension loading (see D27685981)
|
||||
// Work around per https://github.com/electron/electron/issues/23662#issuecomment-787420799
|
||||
const reactDevToolsPath = `${os.homedir()}/Library/Application Support/Electron/extensions/${
|
||||
REACT_DEVELOPER_TOOLS.id
|
||||
}`;
|
||||
if (await promisify(fs.exists)(reactDevToolsPath)) {
|
||||
console.log('Loading React devtools from disk ' + reactDevToolsPath);
|
||||
try {
|
||||
await session.defaultSession.loadExtension(
|
||||
reactDevToolsPath,
|
||||
// @ts-ignore only supported (and needed) in Electron 12
|
||||
{allowFileAccess: true},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to load React devtools from disk: ', e);
|
||||
}
|
||||
})
|
||||
.catch((e: any) => console.error('Error while delegating app launch', e));
|
||||
} else {
|
||||
try {
|
||||
await installExtension(REACT_DEVELOPER_TOOLS.id, {
|
||||
loadExtensionOptions: {allowFileAccess: true, forceDownload},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to install React devtools extension', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.on('web-contents-created', (_event, contents) => {
|
||||
|
||||
@@ -4604,11 +4604,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1"
|
||||
integrity sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==
|
||||
|
||||
"@types/which@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.1.tgz#27ecd67f915b7c3d6ba552135bb1eecd66e63501"
|
||||
integrity sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==
|
||||
|
||||
"@types/ws@^8.5.3":
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
||||
|
||||
Reference in New Issue
Block a user