Merge branch 'main' of github.com:facebook/flipper into universalBuild

This commit is contained in:
2023-10-31 10:13:43 +01:00
174 changed files with 2087 additions and 7444 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -12,3 +12,4 @@ tsc-error.log
/flipper-server/static/
/static/flipper-server-log*
/static/.audit.json
/static/icons/*_d.png

View File

@@ -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);

View File

@@ -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,

View File

@@ -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)) {

View 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: '',
};
}

View File

@@ -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}.`,

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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": {

View File

@@ -20,7 +20,6 @@ type Icon = {
name: string;
variant: 'outline' | 'filled';
size: number;
density: number;
};
interface NotificationAction {

View File

@@ -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",

View File

@@ -7,7 +7,7 @@
* @format
*/
import React, {CSSProperties, forwardRef} from 'react';
import React from 'react';
import styled from '@emotion/styled';
import {
normalizePadding,

View File

@@ -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';

View File

@@ -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}>

View File

@@ -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};

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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);
}}

View File

@@ -673,7 +673,7 @@ export function DataTable<T extends object>(
<Searchbar gap>
<PowerSearch
config={powerSearchConfig}
initialSearchExpression={searchExpression}
searchExpression={searchExpression}
onSearchExpressionChange={(newSearchExpression) => {
tableManager.setSearchExpression(newSearchExpression);
}}

View File

@@ -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';

View File

@@ -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;
},

View File

@@ -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"
},

View File

@@ -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) {

View File

@@ -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);
}
});
}

View File

@@ -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);

View File

@@ -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> => {

View File

@@ -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;

View File

@@ -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';

View File

@@ -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);
});
});

View File

@@ -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';

View File

@@ -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,

View 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;
}
}
}

View File

@@ -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};
}

View File

@@ -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;
}
}

View File

@@ -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}});
};

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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;
},

View File

@@ -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.');

View File

@@ -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,

View File

@@ -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() ||

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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

View File

@@ -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>

View File

@@ -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 = (
<>

View File

@@ -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);

View File

@@ -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}
/>
);
}

View File

@@ -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,
});
}
}

View File

@@ -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`;
}

View File

@@ -171,7 +171,7 @@
"npm": "use yarn instead",
"yarn": "^1.16"
},
"version": "0.227.0",
"version": "0.233.0",
"workspaces": {
"packages": [
"scripts",

View File

@@ -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'
// ...
}

View File

@@ -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'
}
```

View File

@@ -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'
}
```

View File

@@ -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.

View File

@@ -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 = {

View File

@@ -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>

View File

@@ -126,7 +126,7 @@ export const Inspector: React.FC<Props> = ({
frameworkEventMetadata={frameworkEventMetadata}
node={selectedNode}
events={selectedFrameworkEvents}
showExtra={showExtra}
showBottomPanel={showExtra}
/>
</Tab>
)}

View File

@@ -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);

View File

@@ -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,
}) => {

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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');

View File

@@ -33,6 +33,7 @@
"peerDependencies": {
"@ant-design/icons": "*",
"@emotion/styled": "*",
"@emotion/css": "*",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",

View 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,
);
});
}

View File

@@ -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:

View File

@@ -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);
}

View File

@@ -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`);
}

View File

@@ -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);

View File

@@ -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.');

View File

@@ -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'];
}

View File

@@ -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(

View File

@@ -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]')

View File

@@ -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"
]

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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"