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:
Andrey Goncharov
2022-09-15 10:02:19 -07:00
committed by Facebook GitHub Bot
parent 2090120cda
commit 97b8b8a1c4
86 changed files with 813 additions and 645 deletions

View File

@@ -0,0 +1,71 @@
/**
* 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 {createState} from '../atom';
test('can subscribe to atom state changes', () => {
const state = createState('abc');
let receivedValue: string | undefined;
let receivedPrevValue: string | undefined;
const unsubscribe = state.subscribe((value, prevValue) => {
receivedValue = value;
receivedPrevValue = prevValue;
});
try {
state.set('def');
expect(receivedValue).toBe('def');
expect(receivedPrevValue).toBe('abc');
state.set('ghi');
expect(receivedValue).toBe('ghi');
expect(receivedPrevValue).toBe('def');
} finally {
unsubscribe();
}
});
test('can unsubscribe from atom state changes', () => {
const state = createState('abc');
let receivedValue: string | undefined;
let receivedPrevValue: string | undefined;
const unsubscribe = state.subscribe((value, prevValue) => {
receivedValue = value;
receivedPrevValue = prevValue;
});
try {
state.set('def');
expect(receivedValue).toBe('def');
expect(receivedPrevValue).toBe('abc');
} finally {
unsubscribe();
}
state.set('ghi');
expect(receivedValue).toBe('def');
expect(receivedPrevValue).toBe('abc');
});
test('can unsubscribe from atom state changes using unsubscribe method', () => {
const state = createState('abc');
let receivedValue: string | undefined;
let receivedPrevValue: string | undefined;
const listener = (value: string, prevValue: string) => {
receivedValue = value;
receivedPrevValue = prevValue;
};
state.subscribe(listener);
try {
state.set('def');
expect(receivedValue).toBe('def');
expect(receivedPrevValue).toBe('abc');
} finally {
state.unsubscribe(listener);
}
state.set('ghi');
expect(receivedValue).toBe('def');
expect(receivedPrevValue).toBe('abc');
});

View File

@@ -0,0 +1,141 @@
/**
* 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 {produce, Draft, enableMapSet} from 'immer';
import {
getCurrentPluginInstance,
Persistable,
registerStorageAtom,
} from '../plugin/PluginBase';
import {
deserializeShallowObject,
makeShallowSerializable,
} from '../utils/shallowSerialization';
enableMapSet();
export interface ReadOnlyAtom<T> {
get(): T;
subscribe(listener: (value: T, prevValue: T) => void): () => void;
unsubscribe(listener: (value: T, prevValue: T) => void): void;
}
export interface Atom<T> extends ReadOnlyAtom<T> {
set(newValue: T): void;
update(recipe: (draft: Draft<T>) => void): void;
update<X extends T>(recipe: (draft: X) => void): void;
}
export class AtomValue<T> implements Atom<T>, Persistable {
value: T;
listeners: ((value: T, prevValue: T) => void)[] = [];
constructor(initialValue: T) {
this.value = initialValue;
}
get() {
return this.value;
}
set(nextValue: T) {
if (nextValue !== this.value) {
const prevValue = this.value;
this.value = nextValue;
this.notifyChanged(prevValue);
}
}
deserialize(value: T) {
this.set(deserializeShallowObject(value));
}
serialize() {
return makeShallowSerializable(this.get());
}
update(recipe: (draft: Draft<T>) => void) {
this.set(produce(this.value, recipe));
}
notifyChanged(prevValue: T) {
// TODO: add scheduling
this.listeners.slice().forEach((l) => l(this.value, prevValue));
}
subscribe(listener: (value: T, prevValue: T) => void) {
this.listeners.push(listener);
return () => this.unsubscribe(listener);
}
unsubscribe(listener: (value: T, prevValue: T) => void) {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) {
this.listeners.splice(idx, 1);
}
}
}
type StateOptions = {
/**
* Should this state persist when exporting a plugin?
* If set, the atom will be saved / loaded under the key provided
*/
persist?: string;
/**
* Store this state in local storage, instead of as part of the plugin import / export.
* State stored in local storage is shared between the same plugin
* across multiple clients/ devices, but not actively synced.
*/
persistToLocalStorage?: boolean;
};
export function createState<T>(
initialValue: T,
options?: StateOptions,
): Atom<T>;
export function createState<T>(): Atom<T | undefined>;
export function createState(
initialValue: any = undefined,
options: StateOptions = {},
): Atom<any> {
const atom = new AtomValue(initialValue);
if (options?.persistToLocalStorage) {
syncAtomWithLocalStorage(options, atom);
} else {
registerStorageAtom(options.persist, atom);
}
return atom;
}
function syncAtomWithLocalStorage(options: StateOptions, atom: AtomValue<any>) {
if (!options?.persist) {
throw new Error(
"The 'persist' option should be set when 'persistToLocalStorage' is set",
);
}
const pluginInstance = getCurrentPluginInstance();
if (!pluginInstance) {
throw new Error(
"The 'persistToLocalStorage' option cannot be used outside a plugin definition",
);
}
const storageKey = `flipper:${pluginInstance.definition.id}:atom:${options.persist}`;
const storedValue = window.localStorage.getItem(storageKey);
if (storedValue != null) {
atom.deserialize(JSON.parse(storedValue));
}
atom.subscribe(() => {
window.localStorage.setItem(storageKey, JSON.stringify(atom.serialize()));
});
}
export function isAtom(value: any): value is Atom<any> {
return value instanceof AtomValue;
}

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
*/
export let batch: (callback: (...args: any[]) => void) => void = (callback) =>
callback();
export const setBatchedUpdateImplementation = (
impl: (callback: (...args: any[]) => void) => void,
) => {
batch = impl;
};
export function batched<T extends Function>(fn: T): T;
export function batched(fn: any) {
return function (this: any) {
let res: any;
batch(() => {
// eslint-disable-next-line
res = fn.apply(this, arguments);
});
return res;
};
}

View File

@@ -0,0 +1,43 @@
/**
* 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 {
DataSource,
createDataSource as baseCreateDataSource,
DataSourceOptions as BaseDataSourceOptions,
DataSourceOptionKey as BaseDataSourceOptionKey,
} from '../data-source/DataSource';
import {registerStorageAtom} from '../plugin/PluginBase';
type DataSourceOptions = BaseDataSourceOptions & {
/**
* Should this state persist when exporting a plugin?
* If set, the dataSource will be saved / loaded under the key provided
*/
persist?: string;
};
export function createDataSource<T, Key extends keyof T>(
initialSet: readonly T[],
options: DataSourceOptions & BaseDataSourceOptionKey<Key>,
): DataSource<T, T[Key] extends string | number ? T[Key] : never>;
export function createDataSource<T>(
initialSet?: readonly T[],
options?: DataSourceOptions,
): DataSource<T, never>;
export function createDataSource<T, Key extends keyof T>(
initialSet: readonly T[] = [],
options?: DataSourceOptions & BaseDataSourceOptionKey<Key>,
): DataSource<T, T[Key] extends string | number ? T[Key] : never> {
const ds = options
? baseCreateDataSource(initialSet, options)
: baseCreateDataSource(initialSet);
registerStorageAtom(options?.persist, ds);
return ds;
}