Rename second batch for '.ts' files to '.tsx' in plugin-lib dir
Summary: Rename first batch for '.ts' files to '.tsx' Reviewed By: passy Differential Revision: D33843245 fbshipit-source-id: 28fd161e258e4d20200620b083aea48f9e58ea64
This commit is contained in:
committed by
Facebook GitHub Bot
parent
75f874a5dd
commit
69bac4a3d6
293
desktop/plugin-lib/src/pluginInstaller.tsx
Normal file
293
desktop/plugin-lib/src/pluginInstaller.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Heavy use of nested promises but without sacrificing error propagation.
|
||||
/* eslint-disable promise/no-nesting */
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {promisify} from 'util';
|
||||
import {PluginManager as PM} from 'live-plugin-manager';
|
||||
import decompress from 'decompress';
|
||||
import decompressTargz from 'decompress-targz';
|
||||
import decompressUnzip from 'decompress-unzip';
|
||||
import tmp from 'tmp';
|
||||
import {InstalledPluginDetails} from 'flipper-common';
|
||||
import {getInstalledPluginDetails, isPluginDir} from './getPluginDetails';
|
||||
import {
|
||||
getPluginVersionInstallationDir,
|
||||
getPluginDirNameFromPackageName,
|
||||
getPluginInstallationDir,
|
||||
pluginInstallationDir,
|
||||
legacyPluginInstallationDir,
|
||||
} from './pluginPaths';
|
||||
import pfilter from 'p-filter';
|
||||
import pmap from 'p-map';
|
||||
import semver from 'semver';
|
||||
import {notNull} from './typeUtils';
|
||||
|
||||
function providePluginManagerNoDependencies(): PM {
|
||||
return new PM({ignoredDependencies: [/.*/]});
|
||||
}
|
||||
|
||||
async function installPluginFromTempDir(
|
||||
sourceDir: string,
|
||||
): Promise<InstalledPluginDetails> {
|
||||
const pluginDetails = await getInstalledPluginDetails(sourceDir);
|
||||
const {name, version} = pluginDetails;
|
||||
const backupDir = path.join(await promisify(tmp.dir)(), `${name}-${version}`);
|
||||
const destinationDir = getPluginVersionInstallationDir(name, version);
|
||||
|
||||
if (pluginDetails.specVersion == 1) {
|
||||
throw new Error(
|
||||
`Cannot install plugin ${pluginDetails.name} because it is packaged using the unsupported format v1. Please encourage the plugin author to update to v2, following the instructions on https://fbflipper.com/docs/extending/js-setup#migration-to-the-new-plugin-specification`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Moving the existing destination dir to backup
|
||||
if (await fs.pathExists(destinationDir)) {
|
||||
await fs.move(destinationDir, backupDir, {overwrite: true});
|
||||
}
|
||||
await fs.move(sourceDir, destinationDir);
|
||||
} catch (err) {
|
||||
// Restore previous version from backup if installation failed
|
||||
await fs.remove(destinationDir);
|
||||
if (await fs.pathExists(backupDir)) {
|
||||
await fs.move(backupDir, destinationDir, {overwrite: true});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return await getInstalledPluginDetails(destinationDir);
|
||||
}
|
||||
|
||||
async function getPluginRootDir(dir: string) {
|
||||
// npm packages are tar.gz archives containing folder 'package' inside
|
||||
const packageDir = path.join(dir, 'package');
|
||||
const isNpmPackage = await fs.pathExists(packageDir);
|
||||
|
||||
// vsix packages are zip archives containing folder 'extension' inside
|
||||
const extensionDir = path.join(dir, 'extension');
|
||||
const isVsix = await fs.pathExists(extensionDir);
|
||||
|
||||
if (!isNpmPackage && !isVsix) {
|
||||
throw new Error(
|
||||
'Package format is invalid: directory "package" or "extensions" not found in the archive root',
|
||||
);
|
||||
}
|
||||
|
||||
return isNpmPackage ? packageDir : extensionDir;
|
||||
}
|
||||
|
||||
export async function getInstalledPlugin(
|
||||
name: string,
|
||||
version: string,
|
||||
): Promise<InstalledPluginDetails | null> {
|
||||
const dir = getPluginVersionInstallationDir(name, version);
|
||||
if (!(await fs.pathExists(dir))) {
|
||||
return null;
|
||||
}
|
||||
return await getInstalledPluginDetails(dir);
|
||||
}
|
||||
|
||||
export async function installPluginFromNpm(name: string) {
|
||||
const tmpDir = await promisify(tmp.dir)();
|
||||
try {
|
||||
await fs.ensureDir(tmpDir);
|
||||
const plugManNoDep = providePluginManagerNoDependencies();
|
||||
plugManNoDep.options.pluginsPath = tmpDir;
|
||||
await plugManNoDep.install(name);
|
||||
const pluginTempDir = path.join(
|
||||
tmpDir,
|
||||
getPluginDirNameFromPackageName(name),
|
||||
);
|
||||
return await installPluginFromTempDir(pluginTempDir);
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installPluginFromFile(
|
||||
packagePath: string,
|
||||
): Promise<InstalledPluginDetails> {
|
||||
const tmpDir = await promisify(tmp.dir)();
|
||||
try {
|
||||
const files = await decompress(packagePath, tmpDir, {
|
||||
plugins: [decompressTargz(), decompressUnzip()],
|
||||
});
|
||||
if (!files.length) {
|
||||
throw new Error('The package is not in tar.gz format or is empty');
|
||||
}
|
||||
const pluginDir = await getPluginRootDir(tmpDir);
|
||||
return await installPluginFromTempDir(pluginDir);
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePlugin(name: string): Promise<void> {
|
||||
await fs.remove(getPluginInstallationDir(name));
|
||||
}
|
||||
|
||||
export async function removePlugins(names: Array<string>): Promise<void> {
|
||||
await pmap(names, (name) => removePlugin(name));
|
||||
}
|
||||
|
||||
export async function getAllInstalledPluginVersions(): Promise<
|
||||
InstalledPluginDetails[]
|
||||
> {
|
||||
const pluginDirs = await getInstalledPluginVersionDirs();
|
||||
const versionDirs = pluginDirs.map(([_, versionDirs]) => versionDirs).flat();
|
||||
return await pmap(versionDirs, (versionDir) =>
|
||||
getInstalledPluginDetails(versionDir).catch((err) => {
|
||||
console.error(`Failed to load plugin details from ${versionDir}`, err);
|
||||
return null;
|
||||
}),
|
||||
).then((versionDetails) => versionDetails.filter(notNull));
|
||||
}
|
||||
|
||||
export async function getInstalledPlugins(): Promise<InstalledPluginDetails[]> {
|
||||
const versionDirs = await getInstalledPluginVersionDirs();
|
||||
return pmap(
|
||||
versionDirs
|
||||
.filter(([_, versionDirs]) => versionDirs.length > 0)
|
||||
.map(([_, versionDirs]) => versionDirs[0]),
|
||||
(latestVersionDir) =>
|
||||
getInstalledPluginDetails(latestVersionDir).catch((err) => {
|
||||
console.error(`Failed to load plugin from ${latestVersionDir}`, err);
|
||||
return null;
|
||||
}),
|
||||
).then((plugins) => plugins.filter(notNull));
|
||||
}
|
||||
|
||||
export async function cleanupOldInstalledPluginVersions(
|
||||
maxNumberOfVersionsToKeep: number,
|
||||
): Promise<void> {
|
||||
const versionDirs = await getInstalledPluginVersionDirs();
|
||||
const versionDirsToDelete = versionDirs
|
||||
.map(([_, versionDirs]) => versionDirs.slice(maxNumberOfVersionsToKeep))
|
||||
.flat();
|
||||
await pmap(versionDirsToDelete, (versionDirToDelete) =>
|
||||
fs.remove(versionDirToDelete).catch(() => {}),
|
||||
);
|
||||
}
|
||||
|
||||
// Before that we installed all plugins to "thirdparty" folder and only kept
|
||||
// a single version for each of them. Now we install plugins to "installed-plugins"
|
||||
// folder and keep multiple versions. This function checks if the legacy folder exists and
|
||||
// moves all the plugins installed there to the new folder.
|
||||
export async function moveInstalledPluginsFromLegacyDir() {
|
||||
if (await fs.pathExists(legacyPluginInstallationDir)) {
|
||||
await fs
|
||||
.readdir(legacyPluginInstallationDir)
|
||||
.then((dirs) =>
|
||||
dirs.map((dir) => path.join(legacyPluginInstallationDir, dir)),
|
||||
)
|
||||
.then((dirs) =>
|
||||
pfilter(dirs, (dir) =>
|
||||
fs
|
||||
.lstat(dir)
|
||||
.then((lstat) => lstat.isDirectory())
|
||||
.catch(() => false),
|
||||
),
|
||||
)
|
||||
.then((dirs) =>
|
||||
pmap(dirs, (dir) =>
|
||||
getInstalledPluginDetails(dir).catch(async (err) => {
|
||||
console.error(
|
||||
`Failed to load plugin from ${dir} on moving legacy plugins. Removing it.`,
|
||||
err,
|
||||
);
|
||||
await fs.remove(dir);
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
)
|
||||
.then((plugins) =>
|
||||
pmap(plugins.filter(notNull), (plugin) =>
|
||||
fs.move(
|
||||
plugin.dir,
|
||||
getPluginVersionInstallationDir(plugin.name, plugin.version),
|
||||
{overwrite: true},
|
||||
),
|
||||
),
|
||||
);
|
||||
await fs.remove(legacyPluginInstallationDir);
|
||||
}
|
||||
}
|
||||
|
||||
type InstalledPluginVersionDirs = [string, string[]][];
|
||||
|
||||
async function getInstalledPluginVersionDirs(): Promise<InstalledPluginVersionDirs> {
|
||||
if (!(await fs.pathExists(pluginInstallationDir))) {
|
||||
return [];
|
||||
}
|
||||
return await fs
|
||||
.readdir(pluginInstallationDir)
|
||||
.then((dirs) => dirs.map((dir) => path.join(pluginInstallationDir, dir)))
|
||||
.then((dirs) =>
|
||||
pfilter(dirs, (dir) =>
|
||||
fs
|
||||
.lstat(dir)
|
||||
.then((lstat) => lstat.isDirectory())
|
||||
.catch(() => false),
|
||||
),
|
||||
)
|
||||
.then((dirs) =>
|
||||
pmap(dirs, (dir) =>
|
||||
fs
|
||||
.readdir(dir)
|
||||
.then((versionDirs) => versionDirs.filter((d) => semver.valid(d)))
|
||||
.then((versionDirs) =>
|
||||
versionDirs.sort((v1, v2) => semver.compare(v2, v1, true)),
|
||||
)
|
||||
.then((versionDirs) =>
|
||||
versionDirs.map((versionDir) => path.join(dir, versionDir)),
|
||||
)
|
||||
.then((versionDirs) =>
|
||||
pfilter(versionDirs, (versionDir) =>
|
||||
fs
|
||||
.lstat(versionDir)
|
||||
.then((lstat) => lstat.isDirectory())
|
||||
.catch(() => false),
|
||||
),
|
||||
),
|
||||
).then((allDirs) =>
|
||||
allDirs.reduce<InstalledPluginVersionDirs>((agg, versionDirs, i) => {
|
||||
agg.push([dirs[i], versionDirs]);
|
||||
return agg;
|
||||
}, []),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAllInstalledPluginsInDir(
|
||||
dir: string,
|
||||
recursive: boolean = false,
|
||||
): Promise<InstalledPluginDetails[]> {
|
||||
const plugins: InstalledPluginDetails[] = [];
|
||||
if (!((await fs.pathExists(dir)) && (await fs.stat(dir)).isDirectory())) {
|
||||
console.log('defaultPlugins dir not found');
|
||||
return plugins;
|
||||
}
|
||||
const items = await fs.readdir(dir);
|
||||
await pmap(items, async (item) => {
|
||||
const fullPath = path.join(dir, item);
|
||||
if (await isPluginDir(fullPath)) {
|
||||
try {
|
||||
plugins.push(await getInstalledPluginDetails(fullPath));
|
||||
} catch (err) {
|
||||
console.error(`Failed to load plugin from ${fullPath}`);
|
||||
}
|
||||
} else if (recursive) {
|
||||
plugins.push(...(await getAllInstalledPluginsInDir(fullPath, recursive)));
|
||||
}
|
||||
});
|
||||
return plugins;
|
||||
}
|
||||
Reference in New Issue
Block a user