Remove plugin compilation and loading from startup hot path
Summary:
- Removed compilation on startup which is not required anymore after switching to plugin spec v2.
- Removed from Node process ("static" package) all the dependencies which are not required anymore: e.g. metro, babel etc.
- Plugin loading code from node process moved to browser process and made asyncronous.
Some expected benefits after these changes:
1) Reduced size of Flipper bundle (~4.5MB reduction for lzma package in my tests) as well as startup time. It's hard to say the exact startup time difference as it is very machine-dependent, and on my machine it was already fast ~1500ms (vs 5500ms for p95) and decreased by just 100ms. But I think we should definitely see some improvements on "launch time" analytics graph for p95/p99.
2) Plugin loading is async now and happens when UI is already shown, so perceptive startup time should be also better now.
3) All plugin loading code is now consolidated in "app/dispatcher/plugins.tsx" instead of being splitted between Node and Browser processes as before. So it will be easier to debug plugin loading.
4) Now it is possible to apply updates of plugins by simple refresh of browser window instead of full Electron process restart as before.
5) 60% less code in Node process. This is good because it is harder to debug changes in Node process than in Browser process, especially taking in account differences between dev/release builds. Because of this Node process often ended up broken after changes. Hopefully now it will be more stable.
Changelog: changed the way of plugin loading, and removed obsolete dependencies, which should reduce bundle size and startup time.
Reviewed By: passy
Differential Revision: D23682756
fbshipit-source-id: 8445c877234b41c73853cebe585e2fdb1638b2c9
This commit is contained in:
committed by
Facebook GitHub Bot
parent
75e7261d1e
commit
f03d5d94ed
@@ -37,6 +37,7 @@
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"open": "^7.0.0",
|
||||
"openssl-wrapper": "^0.3.4",
|
||||
"p-map": "^4.0.0",
|
||||
"promise-retry": "^1.1.1",
|
||||
"promisify-child-process": "^4.1.0",
|
||||
"prop-types": "^15.6.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.3.1",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"redux": "^4.0.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rsocket-core": "^0.0.19",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
jest.mock('../../defaultPlugins');
|
||||
jest.mock('../../utils/loadDynamicPlugins');
|
||||
import dispatcher, {
|
||||
getDynamicPlugins,
|
||||
checkDisabled,
|
||||
@@ -17,7 +18,7 @@ import dispatcher, {
|
||||
} from '../plugins';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
import path from 'path';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
import {remote} from 'electron';
|
||||
import {FlipperPlugin} from 'flipper';
|
||||
import reducers, {State} from '../../reducers/index';
|
||||
import {getInstance} from '../../fb-stubs/Logger';
|
||||
@@ -26,6 +27,10 @@ import {TEST_PASSING_GK, TEST_FAILING_GK} from '../../fb-stubs/GK';
|
||||
import TestPlugin from './TestPlugin';
|
||||
import {resetConfigForTesting} from '../../utils/processConfig';
|
||||
import {SandyPluginDefinition} from 'flipper-plugin';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
import loadDynamicPlugins from '../../utils/loadDynamicPlugins';
|
||||
|
||||
const loadDynamicPluginsMock = mocked(loadDynamicPlugins);
|
||||
|
||||
const mockStore = configureStore<State, {}>([])(
|
||||
reducers(undefined, {type: 'INIT'}),
|
||||
@@ -47,33 +52,26 @@ const samplePluginDetails: PluginDetails = {
|
||||
|
||||
beforeEach(() => {
|
||||
resetConfigForTesting();
|
||||
loadDynamicPluginsMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
test('dispatcher dispatches REGISTER_PLUGINS', () => {
|
||||
dispatcher(mockStore, logger);
|
||||
afterEach(() => {
|
||||
loadDynamicPluginsMock.mockClear();
|
||||
});
|
||||
|
||||
test('dispatcher dispatches REGISTER_PLUGINS', async () => {
|
||||
await dispatcher(mockStore, logger);
|
||||
const actions = mockStore.getActions();
|
||||
expect(actions.map((a) => a.type)).toContain('REGISTER_PLUGINS');
|
||||
});
|
||||
|
||||
test('getDynamicPlugins returns empty array on errors', () => {
|
||||
const sendSyncMock = jest.fn();
|
||||
sendSyncMock.mockImplementation(() => {
|
||||
throw new Error('ooops');
|
||||
});
|
||||
ipcRenderer.sendSync = sendSyncMock;
|
||||
const res = getDynamicPlugins();
|
||||
test('getDynamicPlugins returns empty array on errors', async () => {
|
||||
const loadDynamicPluginsMock = mocked(loadDynamicPlugins);
|
||||
loadDynamicPluginsMock.mockRejectedValue(new Error('ooops'));
|
||||
const res = await getDynamicPlugins();
|
||||
expect(res).toEqual([]);
|
||||
});
|
||||
|
||||
test('getDynamicPlugins from main process via ipc', () => {
|
||||
const plugins = [{name: 'test'}];
|
||||
const sendSyncMock = jest.fn();
|
||||
sendSyncMock.mockReturnValue(plugins);
|
||||
ipcRenderer.sendSync = sendSyncMock;
|
||||
const res = getDynamicPlugins();
|
||||
expect(res).toEqual(plugins);
|
||||
});
|
||||
|
||||
test('checkDisabled', () => {
|
||||
const disabledPlugin = 'pluginName';
|
||||
const config = {disabledPlugins: [disabledPlugin]};
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
addDisabledPlugins,
|
||||
addFailedPlugins,
|
||||
} from '../reducers/plugins';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import GK from '../fb-stubs/GK';
|
||||
import {FlipperBasePlugin} from '../plugin';
|
||||
import {setupMenuBar} from '../MenuBar';
|
||||
@@ -33,13 +32,14 @@ import semver from 'semver';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics';
|
||||
import * as FlipperPluginSDK from 'flipper-plugin';
|
||||
import {SandyPluginDefinition} from 'flipper-plugin';
|
||||
import loadDynamicPlugins from '../utils/loadDynamicPlugins';
|
||||
import Immer from 'immer';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import getPluginIndex from '../utils/getDefaultPluginsIndex';
|
||||
import {SandyPluginDefinition} from 'flipper-plugin';
|
||||
|
||||
export default (store: Store, logger: Logger) => {
|
||||
export default async (store: Store, logger: Logger) => {
|
||||
// expose Flipper and exact globally for dynamically loaded plugins
|
||||
const globalObject: any = typeof window === 'undefined' ? global : window;
|
||||
globalObject.React = React;
|
||||
@@ -57,7 +57,7 @@ export default (store: Store, logger: Logger) => {
|
||||
|
||||
const initialPlugins: PluginDefinition[] = filterNewestVersionOfEachPlugin(
|
||||
getBundledPlugins(),
|
||||
getDynamicPlugins(),
|
||||
await getDynamicPlugins(),
|
||||
)
|
||||
.map(reportVersion)
|
||||
.filter(checkDisabled(disabledPlugins))
|
||||
@@ -134,14 +134,13 @@ function getBundledPlugins(): Array<PluginDetails> {
|
||||
return bundledPlugins;
|
||||
}
|
||||
|
||||
export function getDynamicPlugins() {
|
||||
let dynamicPlugins: Array<PluginDetails> = [];
|
||||
export async function getDynamicPlugins() {
|
||||
try {
|
||||
dynamicPlugins = ipcRenderer.sendSync('get-dynamic-plugins');
|
||||
return await loadDynamicPlugins();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error('Failed to load dynamic plugins', e);
|
||||
return [];
|
||||
}
|
||||
return dynamicPlugins;
|
||||
}
|
||||
|
||||
export const checkGK = (gatekeepedPlugins: Array<PluginDetails>) => (
|
||||
|
||||
88
desktop/app/src/utils/loadDynamicPlugins.tsx
Normal file
88
desktop/app/src/utils/loadDynamicPlugins.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 pMap from 'p-map';
|
||||
import {
|
||||
PluginDetails,
|
||||
getSourcePlugins,
|
||||
getInstalledPlugins,
|
||||
finishPendingPluginInstallations,
|
||||
} from 'flipper-plugin-lib';
|
||||
import os from 'os';
|
||||
import {getStaticPath} from '../utils/pathUtils';
|
||||
|
||||
const pluginCache = path.join(os.homedir(), '.flipper', 'plugins');
|
||||
|
||||
// Load "dynamic" plugins, e.g. those which are either installed or loaded from sources for development purposes.
|
||||
// This opposed to "static" plugins which are already included into Flipper bundle.
|
||||
export default async function loadDynamicPlugins(): Promise<PluginDetails[]> {
|
||||
if (process.env.FLIPPER_FAST_REFRESH) {
|
||||
console.log(
|
||||
'❌ Skipping loading of dynamic plugins because Fast Refresh is enabled. Fast Refresh only works with bundled plugins.',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await finishPendingPluginInstallations();
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to finish pending installations', err);
|
||||
}
|
||||
const staticPath = getStaticPath();
|
||||
const defaultPlugins = new Set<string>(
|
||||
(
|
||||
await fs.readJson(path.join(staticPath, 'defaultPlugins', 'index.json'))
|
||||
).map((p: any) => p.name) as string[],
|
||||
);
|
||||
const dynamicPlugins = [
|
||||
...(await getInstalledPlugins()),
|
||||
...(await getSourcePlugins()).filter((p) => !defaultPlugins.has(p.name)),
|
||||
];
|
||||
await fs.ensureDir(pluginCache);
|
||||
const compilations = pMap(
|
||||
dynamicPlugins,
|
||||
(plugin) => {
|
||||
return loadPlugin(plugin);
|
||||
},
|
||||
{concurrency: 4},
|
||||
);
|
||||
const compiledDynamicPlugins = (await compilations).filter(
|
||||
(c) => c !== null,
|
||||
) as PluginDetails[];
|
||||
console.log('✅ Loaded all plugins.');
|
||||
return compiledDynamicPlugins;
|
||||
}
|
||||
async function loadPlugin(
|
||||
pluginDetails: PluginDetails,
|
||||
): Promise<PluginDetails | null> {
|
||||
const {specVersion, version, entry, name} = pluginDetails;
|
||||
if (specVersion > 1) {
|
||||
if (await fs.pathExists(entry)) {
|
||||
return pluginDetails;
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Try to load cached version of legacy plugin
|
||||
const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`);
|
||||
if (await fs.pathExists(entry)) {
|
||||
console.log(`🥫 Using cached version of legacy plugin ${name}...`);
|
||||
return pluginDetails;
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because it is defined by the unsupported spec v1 and could not be compiled.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,9 @@
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.4",
|
||||
"expand-tilde": "^2.0.2",
|
||||
"express": "^4.15.2",
|
||||
"fb-watchman": "^2.0.1",
|
||||
"flipper-babel-transformer": "0.57.0",
|
||||
"flipper-pkg-lib": "0.57.0",
|
||||
"flipper-plugin-lib": "0.57.0",
|
||||
@@ -218,6 +220,7 @@
|
||||
"ts-jest": "^26.0.0",
|
||||
"ts-node": "^8.8.1",
|
||||
"typescript": "^3.9.5",
|
||||
"uuid": "^8.3.0",
|
||||
"yargs": "^15.4.1",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
"decompress": "^4.2.1",
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"expand-tilde": "^2.0.2",
|
||||
"fs-extra": "^9.0.1",
|
||||
"live-plugin-manager": "^0.14.1",
|
||||
"npm-api": "^1.0.0",
|
||||
"p-filter": "^2.1.0",
|
||||
"p-map": "^4.0.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"semver": "^7.3.2",
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
|
||||
@@ -10,26 +10,16 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import expandTilde from 'expand-tilde';
|
||||
import {
|
||||
getPluginsInstallationFolder,
|
||||
getPluginSourceFolders,
|
||||
} from './getPluginFolders';
|
||||
import {getPluginSourceFolders} from './pluginPaths';
|
||||
import {PluginDetails, getPluginDetails} from 'flipper-plugin-lib';
|
||||
import pmap from 'p-map';
|
||||
import pfilter from 'p-filter';
|
||||
import {satisfies} from 'semver';
|
||||
|
||||
const flipperVersion = require('./package.json').version;
|
||||
const flipperVersion = require('../package.json').version;
|
||||
|
||||
export async function getSourcePlugins(): Promise<PluginDetails[]> {
|
||||
return await getPluginsFromFolders(await getPluginSourceFolders());
|
||||
}
|
||||
export async function getInstalledPlugins(): Promise<PluginDetails[]> {
|
||||
return await getPluginsFromFolders([getPluginsInstallationFolder()]);
|
||||
}
|
||||
async function getPluginsFromFolders(
|
||||
pluginFolders: string[],
|
||||
): Promise<PluginDetails[]> {
|
||||
const pluginFolders = await getPluginSourceFolders();
|
||||
const entryPoints: {[key: string]: PluginDetails} = {};
|
||||
const additionalPlugins = await pmap(pluginFolders, (path) =>
|
||||
entryPointForPluginFolder(path),
|
||||
@@ -12,3 +12,5 @@ export {default as getPluginDetails} from './getPluginDetails';
|
||||
export * from './pluginInstaller';
|
||||
export * from './getInstalledPlugins';
|
||||
export * from './getUpdatablePlugins';
|
||||
export * from './getSourcePlugins';
|
||||
export {getPluginSourceFolders} from './pluginPaths';
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
import path from 'path';
|
||||
import {homedir} from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import expandTilde from 'expand-tilde';
|
||||
|
||||
export const flipperDataDir = path.join(homedir(), '.flipper');
|
||||
|
||||
@@ -20,3 +22,23 @@ export const pluginPendingInstallationDir = path.join(
|
||||
);
|
||||
|
||||
export const pluginCacheDir = path.join(flipperDataDir, 'plugins');
|
||||
|
||||
export async function getPluginSourceFolders(): Promise<string[]> {
|
||||
const pluginFolders: string[] = [];
|
||||
if (process.env.FLIPPER_NO_EMBEDDED_PLUGINS === 'true') {
|
||||
console.log(
|
||||
'🥫 Skipping embedded plugins because "--no-embedded-plugins" flag provided',
|
||||
);
|
||||
return pluginFolders;
|
||||
}
|
||||
const flipperConfigPath = path.join(homedir(), '.flipper', 'config.json');
|
||||
if (await fs.pathExists(flipperConfigPath)) {
|
||||
const config = await fs.readJson(flipperConfigPath);
|
||||
if (config.pluginPaths) {
|
||||
pluginFolders.push(...config.pluginPaths);
|
||||
}
|
||||
}
|
||||
pluginFolders.push(path.resolve(__dirname, '..', '..', 'plugins'));
|
||||
pluginFolders.push(path.resolve(__dirname, '..', '..', 'plugins', 'fb'));
|
||||
return pluginFolders.map(expandTilde).filter(fs.existsSync);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import fs from 'fs-extra';
|
||||
import {spawn} from 'promisify-child-process';
|
||||
import {getWatchFolders} from 'flipper-pkg-lib';
|
||||
import getAppWatchFolders from './get-app-watch-folders';
|
||||
import {getSourcePlugins} from '../static/getPlugins';
|
||||
import {getSourcePlugins, getPluginSourceFolders} from 'flipper-plugin-lib';
|
||||
import {
|
||||
appDir,
|
||||
staticDir,
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
headlessDir,
|
||||
babelTransformationsDir,
|
||||
} from './paths';
|
||||
import {getPluginSourceFolders} from '../static/getPluginFolders';
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {getPluginSourceFolders} from './getPluginFolders';
|
||||
import {getPluginSourceFolders} from 'flipper-plugin-lib';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const watchmanconfigName = '.watchmanconfig';
|
||||
@@ -20,16 +20,15 @@ import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {hostname} from 'os';
|
||||
import {compileMain, generatePluginEntryPoints} from './build-utils';
|
||||
import Watchman from '../static/watchman';
|
||||
import Watchman from './watchman';
|
||||
import Metro from 'metro';
|
||||
import MetroResolver from 'metro-resolver';
|
||||
import {staticDir, appDir, babelTransformationsDir} from './paths';
|
||||
import isFB from './isFB';
|
||||
import getAppWatchFolders from './get-app-watch-folders';
|
||||
import {getSourcePlugins} from '../static/getPlugins';
|
||||
import {getPluginSourceFolders} from '../static/getPluginFolders';
|
||||
import startWatchPlugins from '../static/startWatchPlugins';
|
||||
import ensurePluginFoldersWatchable from '../static/ensurePluginFoldersWatchable';
|
||||
import {getPluginSourceFolders} from 'flipper-plugin-lib';
|
||||
import ensurePluginFoldersWatchable from './ensurePluginFoldersWatchable';
|
||||
import startWatchPlugins from './startWatchPlugins';
|
||||
|
||||
const ansiToHtmlConverter = new AnsiToHtmlConverter();
|
||||
|
||||
@@ -211,7 +210,7 @@ async function startWatchChanges(io: socketIo.Server) {
|
||||
const watchman = new Watchman(path.resolve(__dirname, '..'));
|
||||
await watchman.initialize();
|
||||
await Promise.all(
|
||||
['app', 'pkg', 'doctor', 'flipper-plugin'].map((dir) =>
|
||||
['app', 'pkg', 'doctor', 'plugin-lib', 'flipper-plugin'].map((dir) =>
|
||||
watchman.startWatchFiles(
|
||||
dir,
|
||||
() => {
|
||||
@@ -223,8 +222,7 @@ async function startWatchChanges(io: socketIo.Server) {
|
||||
),
|
||||
),
|
||||
);
|
||||
const plugins = await getSourcePlugins();
|
||||
await startWatchPlugins(plugins, () => {
|
||||
await startWatchPlugins(() => {
|
||||
io.emit('refresh');
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
52
desktop/scripts/startWatchPlugins.ts
Normal file
52
desktop/scripts/startWatchPlugins.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 Watchman from './watchman';
|
||||
import {getPluginSourceFolders} from 'flipper-plugin-lib';
|
||||
|
||||
export default async function startWatchPlugins(
|
||||
onChanged: () => void | Promise<void>,
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🕵️ Watching for plugin changes');
|
||||
|
||||
let delayedCompilation: NodeJS.Timeout | undefined;
|
||||
const kCompilationDelayMillis = 1000;
|
||||
const onPluginChangeDetected = () => {
|
||||
if (!delayedCompilation) {
|
||||
delayedCompilation = setTimeout(() => {
|
||||
delayedCompilation = undefined;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🕵️ Detected plugin change`);
|
||||
onChanged();
|
||||
}, kCompilationDelayMillis);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await startWatchingPluginsUsingWatchman(onPluginChangeDetected);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to start watching plugin files using Watchman, continue without hot reloading',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function startWatchingPluginsUsingWatchman(onChange: () => void) {
|
||||
const pluginFolders = await getPluginSourceFolders();
|
||||
await Promise.all(
|
||||
pluginFolders.map(async (pluginFolder) => {
|
||||
const watchman = new Watchman(pluginFolder);
|
||||
await watchman.initialize();
|
||||
await watchman.startWatchFiles('.', () => onChange(), {
|
||||
excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'],
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 util from 'util';
|
||||
import recursiveReaddir from 'recursive-readdir';
|
||||
import pMap from 'p-map';
|
||||
import {homedir} from 'os';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
import {runBuild} from 'flipper-pkg-lib';
|
||||
import {getSourcePlugins, getInstalledPlugins} from './getPlugins';
|
||||
import startWatchPlugins from './startWatchPlugins';
|
||||
import ensurePluginFoldersWatchable from './ensurePluginFoldersWatchable';
|
||||
|
||||
const HOME_DIR = homedir();
|
||||
|
||||
const DEFAULT_COMPILE_OPTIONS: CompileOptions = {
|
||||
force: false,
|
||||
failSilently: true,
|
||||
recompileOnChanges: true,
|
||||
};
|
||||
|
||||
export type CompileOptions = {
|
||||
force: boolean;
|
||||
failSilently: boolean;
|
||||
recompileOnChanges: boolean;
|
||||
};
|
||||
|
||||
export default async function (
|
||||
reloadCallback: (() => void) | null,
|
||||
pluginCache: string,
|
||||
options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
|
||||
): Promise<PluginDetails[]> {
|
||||
if (process.env.FLIPPER_FAST_REFRESH) {
|
||||
console.log(
|
||||
'🥫 Skipping loading of installed plugins because Fast Refresh is enabled',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
await ensurePluginFoldersWatchable();
|
||||
options = Object.assign({}, DEFAULT_COMPILE_OPTIONS, options);
|
||||
const defaultPlugins = (
|
||||
await fs.readJson(path.join(__dirname, 'defaultPlugins', 'index.json'))
|
||||
).map((p: any) => p.name) as string[];
|
||||
const dynamicPlugins = [
|
||||
...(await getInstalledPlugins()),
|
||||
...(await getSourcePlugins()).filter(
|
||||
(p) => !defaultPlugins.includes(p.name),
|
||||
),
|
||||
];
|
||||
await fs.ensureDir(pluginCache);
|
||||
if (options.recompileOnChanges) {
|
||||
await startWatchChanges(
|
||||
dynamicPlugins,
|
||||
reloadCallback,
|
||||
pluginCache,
|
||||
options,
|
||||
);
|
||||
}
|
||||
const compilations = pMap(
|
||||
dynamicPlugins,
|
||||
(plugin) => {
|
||||
return compilePlugin(plugin, pluginCache, options);
|
||||
},
|
||||
{concurrency: 4},
|
||||
);
|
||||
|
||||
const compiledDynamicPlugins = (await compilations).filter(
|
||||
(c) => c !== null,
|
||||
) as PluginDetails[];
|
||||
console.log('✅ Compiled all plugins.');
|
||||
return compiledDynamicPlugins;
|
||||
}
|
||||
async function startWatchChanges(
|
||||
plugins: PluginDetails[],
|
||||
reloadCallback: (() => void) | null,
|
||||
pluginCache: string,
|
||||
options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
|
||||
) {
|
||||
const filteredPlugins = plugins
|
||||
// no hot reloading for plugins in .flipper folder. This is to prevent
|
||||
// Flipper from reloading, while we are doing changes on thirdparty plugins.
|
||||
.filter(
|
||||
(plugin) => !plugin.dir.startsWith(path.join(HOME_DIR, '.flipper')),
|
||||
);
|
||||
const watchOptions = Object.assign({}, options, {force: true});
|
||||
await startWatchPlugins(filteredPlugins, (plugin) =>
|
||||
compilePlugin(plugin, pluginCache, watchOptions).then(
|
||||
reloadCallback ?? (() => {}),
|
||||
),
|
||||
);
|
||||
}
|
||||
async function mostRecentlyChanged(dir: string) {
|
||||
const files = await util.promisify<string, string[]>(recursiveReaddir)(dir);
|
||||
return files
|
||||
.map((f) => fs.lstatSync(f).ctime)
|
||||
.reduce((a, b) => (a > b ? a : b), new Date(0));
|
||||
}
|
||||
async function compilePlugin(
|
||||
pluginDetails: PluginDetails,
|
||||
pluginCache: string,
|
||||
{force, failSilently}: CompileOptions,
|
||||
): Promise<PluginDetails | null> {
|
||||
const {dir, specVersion, version, entry, source, name} = pluginDetails;
|
||||
if (specVersion > 1) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (await fs.pathExists(entry)) {
|
||||
console.log(`🥫 Using pre-built version of ${name}: ${entry}...`);
|
||||
return pluginDetails;
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`);
|
||||
const rootDirCtime = await mostRecentlyChanged(dir);
|
||||
if (
|
||||
!force &&
|
||||
(await fs.pathExists(entry)) &&
|
||||
rootDirCtime < (await fs.lstat(entry)).ctime
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🥫 Using cached version of ${name}...`);
|
||||
return pluginDetails;
|
||||
} else {
|
||||
// eslint-disable-line no-console
|
||||
console.log(`⚙️ Compiling ${name}...`);
|
||||
try {
|
||||
await runBuild(dir, source, entry, false);
|
||||
} catch (e) {
|
||||
if (failSilently) {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because it could not be compiled.`,
|
||||
);
|
||||
console.error(e);
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return pluginDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 expandTilde from 'expand-tilde';
|
||||
import {homedir} from 'os';
|
||||
|
||||
export function getPluginsInstallationFolder(): string {
|
||||
return path.join(homedir(), '.flipper', 'thirdparty');
|
||||
}
|
||||
|
||||
export async function getPluginSourceFolders(): Promise<string[]> {
|
||||
const pluginFolders: string[] = [];
|
||||
if (process.env.FLIPPER_NO_EMBEDDED_PLUGINS === 'true') {
|
||||
console.log(
|
||||
'🥫 Skipping embedded plugins because "--no-embedded-plugins" flag provided',
|
||||
);
|
||||
return pluginFolders;
|
||||
}
|
||||
const flipperConfigPath = path.join(homedir(), '.flipper', 'config.json');
|
||||
if (await fs.pathExists(flipperConfigPath)) {
|
||||
const config = await fs.readJson(flipperConfigPath);
|
||||
if (config.pluginPaths) {
|
||||
pluginFolders.push(...config.pluginPaths);
|
||||
}
|
||||
}
|
||||
pluginFolders.push(path.resolve(__dirname, '..', 'plugins'));
|
||||
pluginFolders.push(path.resolve(__dirname, '..', 'plugins', 'fb'));
|
||||
return pluginFolders.map(expandTilde).filter(fs.existsSync);
|
||||
}
|
||||
@@ -23,12 +23,10 @@ import url from 'url';
|
||||
import fs from 'fs';
|
||||
import fixPath from 'fix-path';
|
||||
import {exec} from 'child_process';
|
||||
import compilePlugins from './compilePlugins';
|
||||
import setup from './setup';
|
||||
import isFB from './fb-stubs/isFB';
|
||||
import delegateToLauncher from './launcher';
|
||||
import yargs from 'yargs';
|
||||
import {finishPendingPluginInstallations} from 'flipper-plugin-lib';
|
||||
|
||||
const VERSION: string = (global as any).__VERSION__;
|
||||
|
||||
@@ -89,7 +87,7 @@ const argv = yargs
|
||||
.help()
|
||||
.parse(process.argv.slice(1));
|
||||
|
||||
const {config, configPath, flipperDir} = setup(argv);
|
||||
const {config, configPath} = setup(argv);
|
||||
|
||||
if (isFB && process.env.FLIPPER_FB === undefined) {
|
||||
process.env.FLIPPER_FB = 'true';
|
||||
@@ -100,7 +98,6 @@ process.env.CONFIG = JSON.stringify(config);
|
||||
// possible reference to main app window
|
||||
let win: BrowserWindow;
|
||||
let appReady = false;
|
||||
let pluginsCompiled = false;
|
||||
let deeplinkURL: string | undefined = argv.url;
|
||||
let filePath: string | undefined = argv.file;
|
||||
|
||||
@@ -111,22 +108,6 @@ setInterval(() => {
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
finishPendingPluginInstallations()
|
||||
.then(() =>
|
||||
compilePlugins(() => {
|
||||
if (win) {
|
||||
win.reload();
|
||||
}
|
||||
}, path.join(flipperDir, 'plugins')),
|
||||
)
|
||||
.then((dynamicPlugins) => {
|
||||
ipcMain.on('get-dynamic-plugins', (event) => {
|
||||
event.returnValue = dynamicPlugins;
|
||||
});
|
||||
pluginsCompiled = true;
|
||||
tryCreateWindow();
|
||||
});
|
||||
|
||||
// check if we already have an instance of this app open
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
@@ -185,7 +166,7 @@ app.on('ready', () => {
|
||||
appReady = true;
|
||||
app.commandLine.appendSwitch('scroll-bounce');
|
||||
configureSession();
|
||||
tryCreateWindow();
|
||||
createWindow();
|
||||
// if in development install the react devtools extension
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const {
|
||||
@@ -278,72 +259,70 @@ app.setAsDefaultProtocolClient('flipper');
|
||||
// is workaround suggested in the issue
|
||||
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
|
||||
|
||||
function tryCreateWindow() {
|
||||
if (appReady && pluginsCompiled) {
|
||||
win = new BrowserWindow({
|
||||
show: false,
|
||||
title: 'Flipper',
|
||||
width: config.lastWindowPosition?.width || 1400,
|
||||
height: config.lastWindowPosition?.height || 1000,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
center: true,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
vibrancy: 'sidebar',
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
webSecurity: false,
|
||||
scrollBounce: true,
|
||||
experimentalFeatures: true,
|
||||
nodeIntegration: true,
|
||||
webviewTag: true,
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
});
|
||||
win.once('ready-to-show', () => {
|
||||
win.show();
|
||||
if (argv['open-dev-tools']) {
|
||||
win.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
win.once('close', () => {
|
||||
win.webContents.send('trackUsage', 'exit');
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Removes as a default protocol for debug builds. Because even when the
|
||||
// production application is installed, and one tries to deeplink through
|
||||
// browser, it still looks for the debug one and tries to open electron
|
||||
app.removeAsDefaultProtocolClient('flipper');
|
||||
}
|
||||
const [x, y] = win.getPosition();
|
||||
const [width, height] = win.getSize();
|
||||
// save window position and size
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
...config,
|
||||
lastWindowPosition: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (
|
||||
config.lastWindowPosition &&
|
||||
config.lastWindowPosition.x &&
|
||||
config.lastWindowPosition.y
|
||||
) {
|
||||
win.setPosition(config.lastWindowPosition.x, config.lastWindowPosition.y);
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
show: false,
|
||||
title: 'Flipper',
|
||||
width: config.lastWindowPosition?.width || 1400,
|
||||
height: config.lastWindowPosition?.height || 1000,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
center: true,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
vibrancy: 'sidebar',
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
webSecurity: false,
|
||||
scrollBounce: true,
|
||||
experimentalFeatures: true,
|
||||
nodeIntegration: true,
|
||||
webviewTag: true,
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
});
|
||||
win.once('ready-to-show', () => {
|
||||
win.show();
|
||||
if (argv['open-dev-tools']) {
|
||||
win.webContents.openDevTools();
|
||||
}
|
||||
const entryUrl =
|
||||
process.env.ELECTRON_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
win.loadURL(entryUrl);
|
||||
});
|
||||
win.once('close', () => {
|
||||
win.webContents.send('trackUsage', 'exit');
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Removes as a default protocol for debug builds. Because even when the
|
||||
// production application is installed, and one tries to deeplink through
|
||||
// browser, it still looks for the debug one and tries to open electron
|
||||
app.removeAsDefaultProtocolClient('flipper');
|
||||
}
|
||||
const [x, y] = win.getPosition();
|
||||
const [width, height] = win.getSize();
|
||||
// save window position and size
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
...config,
|
||||
lastWindowPosition: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (
|
||||
config.lastWindowPosition &&
|
||||
config.lastWindowPosition.x &&
|
||||
config.lastWindowPosition.y
|
||||
) {
|
||||
win.setPosition(config.lastWindowPosition.x, config.lastWindowPosition.y);
|
||||
}
|
||||
const entryUrl =
|
||||
process.env.ELECTRON_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
win.loadURL(entryUrl);
|
||||
}
|
||||
|
||||
@@ -6,21 +6,14 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"expand-tilde": "^2.0.2",
|
||||
"fb-watchman": "^2.0.0",
|
||||
"fix-path": "^3.0.0",
|
||||
"flipper-pkg-lib": "0.57.0",
|
||||
"flipper-plugin-lib": "0.57.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"ignore": "^5.1.4",
|
||||
"mac-ca": "^1.0.4",
|
||||
"mem": "^6.0.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"p-filter": "^2.1.0",
|
||||
"p-map": "^4.0.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"semver": "^7.3.2",
|
||||
"uuid": "^8.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ws": "^7.3.0",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"yargs": "^15.4.1"
|
||||
|
||||
@@ -59,5 +59,5 @@ export default function setup(argv: any) {
|
||||
launcherMsg: argv.launcherMsg,
|
||||
};
|
||||
|
||||
return {config, configPath, flipperDir};
|
||||
return {config, configPath};
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 Watchman from './watchman';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
export default async function startWatchPlugins(
|
||||
plugins: PluginDetails[],
|
||||
compilePlugin: (plugin: PluginDetails) => void | Promise<void>,
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🕵️ Watching for plugin changes');
|
||||
|
||||
const delayedCompilation: {[key: string]: NodeJS.Timeout | null} = {};
|
||||
const kCompilationDelayMillis = 1000;
|
||||
const onPluginChanged = (plugin: PluginDetails) => {
|
||||
if (!delayedCompilation[plugin.name]) {
|
||||
delayedCompilation[plugin.name] = setTimeout(() => {
|
||||
delayedCompilation[plugin.name] = null;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🕵️ Detected changes in ${plugin.name}`);
|
||||
compilePlugin(plugin);
|
||||
}, kCompilationDelayMillis);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await startWatchingPluginsUsingWatchman(plugins, onPluginChanged);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to start watching plugin files using Watchman, continue without hot reloading',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function startWatchingPluginsUsingWatchman(
|
||||
plugins: PluginDetails[],
|
||||
onPluginChanged: (plugin: PluginDetails) => void,
|
||||
) {
|
||||
// Initializing a watchman for each folder containing plugins
|
||||
const watchmanRootMap: {[key: string]: Watchman} = {};
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
const watchmanRoot = path.resolve(plugin.dir, '..');
|
||||
if (!watchmanRootMap[watchmanRoot]) {
|
||||
watchmanRootMap[watchmanRoot] = new Watchman(watchmanRoot);
|
||||
await watchmanRootMap[watchmanRoot].initialize();
|
||||
}
|
||||
}),
|
||||
);
|
||||
// Start watching plugins using the initialized watchmans
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
const watchmanRoot = path.resolve(plugin.dir, '..');
|
||||
const watchman = watchmanRootMap[watchmanRoot];
|
||||
await watchman.startWatchFiles(
|
||||
path.relative(watchmanRoot, plugin.dir),
|
||||
() => onPluginChanged(plugin),
|
||||
{
|
||||
excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'],
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -8420,13 +8420,6 @@ makeerror@1.0.x:
|
||||
dependencies:
|
||||
tmpl "1.0.x"
|
||||
|
||||
map-age-cleaner@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
|
||||
integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
|
||||
dependencies:
|
||||
p-defer "^1.0.0"
|
||||
|
||||
map-cache@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
||||
@@ -8473,14 +8466,6 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
mem@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mem/-/mem-6.0.1.tgz#3f8ad1b0f8c4e00daf07f104e95b9d78131d7908"
|
||||
integrity sha512-uIRYASflIsXqvKe+7aXbLrydaRzz4qiK6amqZDQI++eRtW3UoKtnDcGeCAOREgll7YMxO5E4VB9+3B0LFmy96g==
|
||||
dependencies:
|
||||
map-age-cleaner "^0.1.3"
|
||||
mimic-fn "^3.0.0"
|
||||
|
||||
"memoize-one@>=3.1.1 <6":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
@@ -8810,11 +8795,6 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mimic-fn@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.0.0.tgz#76044cfa8818bbf6999c5c9acadf2d3649b14b4b"
|
||||
integrity sha512-PiVO95TKvhiwgSwg1IdLYlCTdul38yZxZMIcnDSFIBUm4BNZha2qpQ4GpJ++15bHoKDtrW2D69lMfFwdFYtNZQ==
|
||||
|
||||
mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||
@@ -8985,6 +8965,11 @@ node-fetch@^1.0.1:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-forge@^0.7.1, node-forge@^0.7.5:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||
@@ -9315,11 +9300,6 @@ p-cancelable@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
|
||||
|
||||
p-defer@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
|
||||
integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
|
||||
|
||||
p-each-series@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
|
||||
@@ -12251,11 +12231,6 @@ uuid@^7.0.3:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||
|
||||
uuid@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
||||
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
||||
|
||||
uuid@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
|
||||
|
||||
Reference in New Issue
Block a user