Split flipper-plugin package
Summary: flipper-server-companion depends on flipper-plugin. flipper-plugin includes dependencies that run only in a browser. Splitting flipper-plugin into core and browser packages helps to avoid including browser-only dependencies into flipper-server bundle. As a result, bundle size could be cut in half. Subsequently, RSS usage drops as there is twice as less code to process for V8. Note: it currently breaks external flipper-data-source package. It will be restored in subsequent diffs Reviewed By: lblasa Differential Revision: D38658285 fbshipit-source-id: 751b11fa9f3a2d938ce166687b8310ba8b059dee
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2090120cda
commit
97b8b8a1c4
@@ -1,15 +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
|
||||
*/
|
||||
|
||||
export interface Idler {
|
||||
shouldIdle(): boolean;
|
||||
idle(): Promise<void>;
|
||||
cancel(): void;
|
||||
isCancelled(): boolean;
|
||||
}
|
||||
@@ -1,32 +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 {Logger} from 'flipper-common';
|
||||
export {Logger} from 'flipper-common';
|
||||
|
||||
export const stubLogger: Logger = {
|
||||
track() {},
|
||||
trackTimeSince() {},
|
||||
info() {
|
||||
// eslint-disable-next-line
|
||||
console.log.apply(console, arguments as any);
|
||||
},
|
||||
warn() {
|
||||
// eslint-disable-next-line
|
||||
console.warn.apply(console, arguments as any);
|
||||
},
|
||||
error() {
|
||||
// eslint-disable-next-line
|
||||
console.error.apply(console, arguments as any);
|
||||
},
|
||||
debug() {
|
||||
// eslint-disable-next-line
|
||||
console.debug.apply(console, arguments as any);
|
||||
},
|
||||
};
|
||||
@@ -1,255 +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 {
|
||||
makeShallowSerializable,
|
||||
deserializeShallowObject,
|
||||
} from '../shallowSerialization';
|
||||
import mockConsole from 'jest-mock-console';
|
||||
|
||||
class TestObject extends Object {
|
||||
constructor(title: Object, map?: Map<any, any>, set?: Set<any>) {
|
||||
super();
|
||||
this.title = title;
|
||||
this.map = map;
|
||||
this.set = set;
|
||||
}
|
||||
title: Object;
|
||||
map?: Map<any, any>;
|
||||
set?: Set<any>;
|
||||
}
|
||||
|
||||
test('test cyclic data structure', () => {
|
||||
const a: any = {x: 0, b: {c: []}};
|
||||
a.b.c.push(a);
|
||||
expect(() => {
|
||||
makeShallowSerializable(a);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cycle detected: object at path '.b.c.0' is referring to itself: '[object Object]'"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('test shared data structure', () => {
|
||||
const restoreConsole = mockConsole();
|
||||
try {
|
||||
const a = {hello: 'world'};
|
||||
const b = {x: a, y: a};
|
||||
|
||||
const res = JSON.parse(JSON.stringify(makeShallowSerializable(b)));
|
||||
expect(res).toEqual({
|
||||
x: {hello: 'world'},
|
||||
y: {hello: 'world'},
|
||||
});
|
||||
expect(b.x).toBe(b.y);
|
||||
expect(res.x).not.toBe(res.y);
|
||||
// @ts-ignore
|
||||
expect(console.warn.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Duplicate value, object lives at path '.y', but also at path '.x': '[object Object]'. This might not behave correct after import and lead to unnecessary big exports.",
|
||||
],
|
||||
]
|
||||
`);
|
||||
} finally {
|
||||
restoreConsole();
|
||||
}
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable function for unnested object with no Set and Map', () => {
|
||||
const obj = {key1: 'value1', key2: 'value2'};
|
||||
const output = makeShallowSerializable(obj);
|
||||
expect(output).toEqual(obj);
|
||||
|
||||
// Testing numbers
|
||||
const obj2 = {key1: 1, key2: 2};
|
||||
const output2 = makeShallowSerializable(obj2);
|
||||
expect(output2).toEqual(obj2);
|
||||
});
|
||||
|
||||
test('makeObjectSerializable function for unnested object with values which returns false when put in an if condition', () => {
|
||||
const obj2 = {key1: 0, key2: ''};
|
||||
const output2 = makeShallowSerializable(obj2);
|
||||
return expect(output2).toEqual(obj2);
|
||||
});
|
||||
|
||||
test('test deserializeShallowObject function for unnested object with no Set and Map', () => {
|
||||
const obj = {key1: 'value1', key2: 'value2'};
|
||||
const output = deserializeShallowObject(obj);
|
||||
expect(output).toEqual(obj);
|
||||
|
||||
// Testing numbers
|
||||
const obj2 = {key1: 1, key2: 2};
|
||||
const output2 = deserializeShallowObject(obj2);
|
||||
expect(output2).toEqual(obj2);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for nested object with no Set and Map', () => {
|
||||
const subObj = {key1: 'value1', key2: 'value2'};
|
||||
const subObj2 = {key21: 'value21', key22: 'value22'};
|
||||
const obj = {key1: subObj, key2: subObj2};
|
||||
const output = makeShallowSerializable(obj);
|
||||
expect(output).toEqual(obj);
|
||||
expect(deserializeShallowObject(output)).toEqual(obj);
|
||||
|
||||
const subObjNum = {key1: 1, key2: 2};
|
||||
const subObjNum2 = {key21: 21, key22: 22};
|
||||
const obj2 = {key1: subObjNum, key2: subObjNum2};
|
||||
const output2 = makeShallowSerializable(obj2);
|
||||
expect(output2).toEqual(obj2);
|
||||
expect(deserializeShallowObject(output2)).toEqual(obj2);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for Map and Set with no nesting', () => {
|
||||
const map = new Map([
|
||||
['k1', 'v1'],
|
||||
['k2', 'v2'],
|
||||
]);
|
||||
const output = makeShallowSerializable(map);
|
||||
const expected = {
|
||||
__flipper_object_type__: 'Map',
|
||||
data: [
|
||||
['k1', 'v1'],
|
||||
['k2', 'v2'],
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
expect(deserializeShallowObject(output)).toEqual(map);
|
||||
|
||||
const set = new Set([1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]);
|
||||
const outputSet = makeShallowSerializable(set);
|
||||
const expectedSet = {
|
||||
__flipper_object_type__: 'Set',
|
||||
data: [1, 2, 3, 4, 5, 6],
|
||||
};
|
||||
expect(outputSet).toEqual(expectedSet);
|
||||
expect(deserializeShallowObject(outputSet)).toEqual(set);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for Map and Set with nesting', () => {
|
||||
const map = new Map([
|
||||
[{title: 'k1'}, {title: 'v1'}],
|
||||
[{title: 'k2'}, {title: 'v2'}],
|
||||
]);
|
||||
const output = makeShallowSerializable(map);
|
||||
const expected = {
|
||||
__flipper_object_type__: 'Map',
|
||||
data: [
|
||||
[{title: 'k1'}, {title: 'v1'}],
|
||||
[{title: 'k2'}, {title: 'v2'}],
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
expect(deserializeShallowObject(output)).toEqual(map);
|
||||
|
||||
const set = new Set([
|
||||
{title: '1'},
|
||||
{title: '2'},
|
||||
{title: '3'},
|
||||
{title: '4'},
|
||||
{title: '5'},
|
||||
{title: '6'},
|
||||
]);
|
||||
const outputSet = makeShallowSerializable(set);
|
||||
const expectedSet = {
|
||||
__flipper_object_type__: 'Set',
|
||||
data: [
|
||||
{title: '1'},
|
||||
{title: '2'},
|
||||
{title: '3'},
|
||||
{title: '4'},
|
||||
{title: '5'},
|
||||
{title: '6'},
|
||||
],
|
||||
};
|
||||
expect(outputSet).toEqual(expectedSet);
|
||||
expect(deserializeShallowObject(outputSet)).toEqual(set);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for custom Object', () => {
|
||||
const obj = new TestObject('title');
|
||||
expect(() => {
|
||||
makeShallowSerializable(obj);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unserializable object type (TestObject) at path '.': [object Object]."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject object with map', () => {
|
||||
const nestedObjWithMap = {
|
||||
map: new Map([
|
||||
['k1', 'v1'],
|
||||
['k2', 'v2'],
|
||||
]),
|
||||
};
|
||||
expect(() => {
|
||||
makeShallowSerializable(nestedObjWithMap);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unserializable object type (Map) at path '.map': [object Map]."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for Array as input', () => {
|
||||
const arr = [1, 2, 4, 5];
|
||||
const output = makeShallowSerializable(arr);
|
||||
expect(output).toEqual(arr);
|
||||
expect(deserializeShallowObject(output)).toEqual(arr);
|
||||
|
||||
const arrMap = [
|
||||
new Map([
|
||||
['a1', 'v1'],
|
||||
['a2', 'v2'],
|
||||
]),
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
makeShallowSerializable(arrMap);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unserializable object type (Map) at path '.0': [object Map]."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('test serialize and deserializeShallowObject function for non Object input', () => {
|
||||
expect(makeShallowSerializable('octopus')).toEqual('octopus');
|
||||
expect(deserializeShallowObject(makeShallowSerializable('octopus'))).toEqual(
|
||||
'octopus',
|
||||
);
|
||||
expect(makeShallowSerializable(24567)).toEqual(24567);
|
||||
expect(deserializeShallowObject(makeShallowSerializable(24567))).toEqual(
|
||||
24567,
|
||||
);
|
||||
});
|
||||
|
||||
// dates on windows don't support changed timezones
|
||||
test.unix(
|
||||
'test makeObjectSerializable and deserializeShallowObject function for Date input',
|
||||
() => {
|
||||
const date = new Date(2021, 1, 29, 10, 31, 7, 205);
|
||||
expect(makeShallowSerializable(date)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"__flipper_object_type__": "Date",
|
||||
"data": 1614555067205,
|
||||
}
|
||||
`);
|
||||
expect(deserializeShallowObject(makeShallowSerializable(date))).toEqual(
|
||||
date,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('test makeObjectSerializable and deserializeShallowObject function for Map of Sets', () => {
|
||||
const map = new Map([
|
||||
['k1', new Set([1, 2, 3, 4, 5, 6])],
|
||||
[new Set([1, 2]), new Map([['k3', 'v3']])],
|
||||
] as any);
|
||||
expect(() => {
|
||||
makeShallowSerializable(map);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unserializable object type (Set) at path '.01': [object Set]."`,
|
||||
);
|
||||
});
|
||||
@@ -8,14 +8,14 @@
|
||||
*/
|
||||
|
||||
import {notification, Typography} from 'antd';
|
||||
import {DataSource} from '../data-source/index';
|
||||
import {DataSource} from 'flipper-plugin-core';
|
||||
import React from 'react';
|
||||
import {PluginClient} from '../plugin/Plugin';
|
||||
import {PluginClient} from 'flipper-plugin-core';
|
||||
import {usePlugin} from '../plugin/PluginContext';
|
||||
import {createState} from '../state/atom';
|
||||
import {createState} from 'flipper-plugin-core';
|
||||
import {DataTableColumn} from '../ui/data-table/DataTable';
|
||||
import {MasterDetail} from '../ui/MasterDetail';
|
||||
import {createDataSource} from '../state/createDataSource';
|
||||
import {createDataSource} from 'flipper-plugin-core';
|
||||
|
||||
type PluginResult<Raw, Row> = {
|
||||
plugin(client: PluginClient<Record<string, Raw | {}>>): {
|
||||
|
||||
@@ -1,285 +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
|
||||
*/
|
||||
|
||||
// Partial clone of the POSIX part of https://github.com/nodejs/node/blob/master/lib/path.js
|
||||
// Docs are copied from https://github.com/nodejs/node/blob/master/doc/api/path.md
|
||||
|
||||
const CHAR_DOT = 46;
|
||||
const CHAR_FORWARD_SLASH = 47;
|
||||
|
||||
function isPosixPathSeparator(code: number) {
|
||||
return code === CHAR_FORWARD_SLASH;
|
||||
}
|
||||
|
||||
// Resolves . and .. elements in a path with directory names
|
||||
function normalizeString(
|
||||
path: string,
|
||||
allowAboveRoot: boolean,
|
||||
separator: string,
|
||||
isPathSeparator: (code: number) => boolean,
|
||||
): string {
|
||||
let res = '';
|
||||
let lastSegmentLength = 0;
|
||||
let lastSlash = -1;
|
||||
let dots = 0;
|
||||
let code = 0;
|
||||
for (let i = 0; i <= path.length; ++i) {
|
||||
if (i < path.length) code = path.charCodeAt(i);
|
||||
else if (isPathSeparator(code)) break;
|
||||
else code = CHAR_FORWARD_SLASH;
|
||||
|
||||
if (isPathSeparator(code)) {
|
||||
if (lastSlash === i - 1 || dots === 1) {
|
||||
// NOOP
|
||||
} else if (dots === 2) {
|
||||
if (
|
||||
res.length < 2 ||
|
||||
lastSegmentLength !== 2 ||
|
||||
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
|
||||
res.charCodeAt(res.length - 2) !== CHAR_DOT
|
||||
) {
|
||||
if (res.length > 2) {
|
||||
const lastSlashIndex = res.lastIndexOf(separator);
|
||||
if (lastSlashIndex === -1) {
|
||||
res = '';
|
||||
lastSegmentLength = 0;
|
||||
} else {
|
||||
res = res.slice(0, lastSlashIndex);
|
||||
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
} else if (res.length !== 0) {
|
||||
res = '';
|
||||
lastSegmentLength = 0;
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
res += res.length > 0 ? `${separator}..` : '..';
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0)
|
||||
res += `${separator}${path.slice(lastSlash + 1, i)}`;
|
||||
else res = path.slice(lastSlash + 1, i);
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
} else if (code === CHAR_DOT && dots !== -1) {
|
||||
++dots;
|
||||
} else {
|
||||
dots = -1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The path.join() method joins all given path segments together using the platform-specific separator as a delimiter, then normalizes the resulting path.
|
||||
* Zero-length path segments are ignored. If the joined path string is a zero-length string then '.' will be returned, representing the current working directory.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
|
||||
* Returns: '/foo/bar/baz/asdf'
|
||||
*/
|
||||
export function join(...args: string[]): string {
|
||||
if (args.length === 0) return '.';
|
||||
let joined;
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
const arg = args[i];
|
||||
if (arg.length > 0) {
|
||||
if (joined === undefined) joined = arg;
|
||||
else joined += `/${arg}`;
|
||||
}
|
||||
}
|
||||
if (joined === undefined) return '.';
|
||||
return normalize(joined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path.normalize() method normalizes the given path, resolving '..' and '.' segments.
|
||||
* When multiple, sequential path segment separation characters are found (e.g. /), they are replaced by a single instance of /. Trailing separators are preserved.
|
||||
* If the path is a zero-length string, '.' is returned, representing the current working directory.
|
||||
*
|
||||
* @example
|
||||
* path.normalize('/foo/bar//baz/asdf/quux/..');
|
||||
* Returns: '/foo/bar/baz/asdf'
|
||||
*/
|
||||
export function normalize(path: string): string {
|
||||
if (path.length === 0) return '.';
|
||||
|
||||
const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
|
||||
const trailingSeparator =
|
||||
path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
|
||||
|
||||
// Normalize the path
|
||||
path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
|
||||
|
||||
if (path.length === 0) {
|
||||
if (isAbsolute) return '/';
|
||||
return trailingSeparator ? './' : '.';
|
||||
}
|
||||
if (trailingSeparator) path += '/';
|
||||
|
||||
return isAbsolute ? `/${path}` : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* The path.extname() method returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last portion of the path. If there is no . in the last portion of the path, or if there are no . characters other than the first character of the basename of path (see path.basename()) , an empty string is returned.
|
||||
*
|
||||
* @example
|
||||
* path.extname('index.html');
|
||||
* Returns: '.html'
|
||||
*
|
||||
* path.extname('index.coffee.md');
|
||||
* Returns: '.md'
|
||||
*
|
||||
* path.extname('index.');
|
||||
* Returns: '.'
|
||||
*
|
||||
* path.extname('index');
|
||||
* Returns: ''
|
||||
*
|
||||
* path.extname('.index');
|
||||
* Returns: ''
|
||||
*
|
||||
* path.extname('.index.md');
|
||||
* Returns: '.md'
|
||||
*/
|
||||
export function extname(path: string): string {
|
||||
let startDot = -1;
|
||||
let startPart = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (code === CHAR_FORWARD_SLASH) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CHAR_DOT) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) startDot = i;
|
||||
else if (preDotState !== 1) preDotState = 1;
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path.basename() method returns the last portion of a path, similar to the Unix basename command. Trailing directory separators are ignored.
|
||||
*
|
||||
* @example
|
||||
* path.basename('/foo/bar/baz/asdf/quux.html');
|
||||
* Returns: 'quux.html'
|
||||
*
|
||||
* path.basename('/foo/bar/baz/asdf/quux.html', '.html');
|
||||
* Returns: 'quux'
|
||||
*/
|
||||
export function basename(path: string, ext?: string) {
|
||||
let start = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
|
||||
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
||||
if (ext === path) return '';
|
||||
let extIdx = ext.length - 1;
|
||||
let firstNonSlashEnd = -1;
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (code === CHAR_FORWARD_SLASH) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (firstNonSlashEnd === -1) {
|
||||
// We saw the first non-path separator, remember this index in case
|
||||
// we need it if the extension ends up not matching
|
||||
matchedSlash = false;
|
||||
firstNonSlashEnd = i + 1;
|
||||
}
|
||||
if (extIdx >= 0) {
|
||||
// Try to match the explicit extension
|
||||
if (code === ext.charCodeAt(extIdx)) {
|
||||
if (--extIdx === -1) {
|
||||
// We matched the extension, so mark this as the end of our path
|
||||
// component
|
||||
end = i;
|
||||
}
|
||||
} else {
|
||||
// Extension does not match, so our result is the entire path
|
||||
// component
|
||||
extIdx = -1;
|
||||
end = firstNonSlashEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end) end = firstNonSlashEnd;
|
||||
else if (end === -1) end = path.length;
|
||||
return path.slice(start, end);
|
||||
}
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// path component
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) return '';
|
||||
return path.slice(start, end);
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {createState, useValue} from '../state/atom';
|
||||
import {createState} from 'flipper-plugin-core';
|
||||
import {useValue} from '../state/atom';
|
||||
import React, {ReactPortal} from 'react';
|
||||
import {createPortal, unmountComponentAtNode} from 'react-dom';
|
||||
|
||||
|
||||
@@ -1,16 +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
|
||||
*/
|
||||
|
||||
export function safeStringify(value: any) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch (e) {
|
||||
return '<Failed to serialize: ' + e + '>';
|
||||
}
|
||||
}
|
||||
@@ -1,186 +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 {isProduction} from 'flipper-common';
|
||||
|
||||
/**
|
||||
* makeShallowSerializable will prepare common data structures, like Map and Set, for JSON serialization.
|
||||
* However, this will happen only for the root object and not recursively to keep things efficiently.
|
||||
*
|
||||
* The function does not take care of actual stringification; use JSON.serialize.
|
||||
*/
|
||||
export function makeShallowSerializable(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
assertSerializable(obj);
|
||||
return obj;
|
||||
}
|
||||
if (obj instanceof Map) {
|
||||
const data = Array.from(obj.entries());
|
||||
assertSerializable(data);
|
||||
return {
|
||||
__flipper_object_type__: 'Map',
|
||||
data,
|
||||
};
|
||||
} else if (obj instanceof Set) {
|
||||
const data = Array.from(obj.values());
|
||||
assertSerializable(data);
|
||||
return {
|
||||
__flipper_object_type__: 'Set',
|
||||
data,
|
||||
};
|
||||
} else if (obj instanceof Date) {
|
||||
return {
|
||||
__flipper_object_type__: 'Date',
|
||||
data: obj.getTime(),
|
||||
};
|
||||
} else {
|
||||
assertSerializable(obj);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of makeShallowSerializable
|
||||
*/
|
||||
export function deserializeShallowObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
if (obj['__flipper_object_type__']) {
|
||||
const type = obj['__flipper_object_type__'];
|
||||
switch (type) {
|
||||
case 'Map': {
|
||||
return new Map(obj.data);
|
||||
}
|
||||
case 'Set': {
|
||||
return new Set(obj.data);
|
||||
}
|
||||
case 'Date':
|
||||
return new Date(obj.data);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a value is JSON serializable.
|
||||
* Will print a warning if a value is JSON serializable, but isn't a pure tree
|
||||
*/
|
||||
export function assertSerializable(obj: any) {
|
||||
if (isProduction()) {
|
||||
return;
|
||||
}
|
||||
// path to current object
|
||||
const path: string[] = [];
|
||||
// current object stack
|
||||
const stack = new Set<any>();
|
||||
// past objects, object -> path to reach it
|
||||
const seen = new Set<any>();
|
||||
|
||||
// to safe a lot of memory allocations, if we find a duplicate, we just start over again to search for the first,
|
||||
// rather than storing all paths at first encounter
|
||||
let duplicateFound = false;
|
||||
let duplicatePath: string[] | undefined;
|
||||
let duplicateObject: any = undefined;
|
||||
let done = false;
|
||||
|
||||
function check(value: any) {
|
||||
if (value === null || done) {
|
||||
return;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case 'undefined':
|
||||
// undefined is not strictly speaking serializable, but behaves fine.
|
||||
// JSON.stringify({x : undefined}) ==> '{}'
|
||||
break;
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
break;
|
||||
case 'object':
|
||||
// A cycle is truly not serializable, as it would create an unending serialization loop...
|
||||
if (stack.has(value)) {
|
||||
throw new Error(
|
||||
`Cycle detected: object at path '.${path.join(
|
||||
'.',
|
||||
)}' is referring to itself: '${value}'`,
|
||||
);
|
||||
}
|
||||
// Encountering an object multiple times is bad, as reference equality will be lost upon
|
||||
// deserialization, so the data isn't properly normalised.
|
||||
// But it *might* work fine, and can serialize, so we just warn
|
||||
|
||||
// Warning is only printed during the second check loop, so that we know *both* paths
|
||||
// - Second walk (which finds first object)
|
||||
if (duplicateFound && duplicateObject && value === duplicateObject) {
|
||||
console.warn(
|
||||
`Duplicate value, object lives at path '.${duplicatePath!.join(
|
||||
'.',
|
||||
)}', but also at path '.${path!.join(
|
||||
'.',
|
||||
)}': '${value}'. This might not behave correct after import and lead to unnecessary big exports.`,
|
||||
);
|
||||
done = true; // no need to finish the second walk
|
||||
break;
|
||||
}
|
||||
// - First walk (which detects the duplicate and stores location of duplicate)
|
||||
if (!duplicateFound) {
|
||||
if (seen.has(value)) {
|
||||
duplicateFound = true;
|
||||
duplicateObject = value;
|
||||
duplicatePath = path.slice();
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
stack.add(value);
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((child, index) => {
|
||||
path.push('' + index);
|
||||
check(child);
|
||||
path.pop();
|
||||
});
|
||||
} else if (proto === null || proto === Object.prototype) {
|
||||
for (const key in value) {
|
||||
path.push(key);
|
||||
check(value[key]);
|
||||
path.pop();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unserializable object type (${
|
||||
proto?.constructor?.name ?? 'Unknown'
|
||||
}) at path '.${path.join('')}': ${value}.`,
|
||||
);
|
||||
}
|
||||
stack.delete(value);
|
||||
break;
|
||||
case 'bigint':
|
||||
case 'function':
|
||||
case 'symbol':
|
||||
default:
|
||||
throw new Error(
|
||||
`Unserializable value (${typeof value}) at path '.${path.join(
|
||||
'.',
|
||||
)}': '${value}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
check(obj);
|
||||
// if there is a duplicate found, re-walk the tree so that we can print both of the paths and report it
|
||||
// this setup is slightly more confusion in code than walking once and storing past paths,
|
||||
// but a lot more efficient :)
|
||||
if (duplicateFound) {
|
||||
path.splice(0);
|
||||
seen.clear();
|
||||
stack.clear();
|
||||
check(obj);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
import {Logger} from 'flipper-common';
|
||||
import {createContext, useContext} from 'react';
|
||||
import {stubLogger} from './Logger';
|
||||
import {_stubLogger} from 'flipper-plugin-core';
|
||||
|
||||
export const _LoggerContext = createContext<Logger>(stubLogger);
|
||||
export const _LoggerContext = createContext<Logger>(_stubLogger);
|
||||
|
||||
/**
|
||||
* Provides the default logger that can be used for console logging,
|
||||
|
||||
Reference in New Issue
Block a user