diff --git a/desktop/app/package.json b/desktop/app/package.json index 16077c106..0d172ad69 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -20,18 +20,15 @@ "algoliasearch": "^4.0.0", "async-mutex": "^0.1.3", "axios": "^0.19.2", - "decompress": "^4.2.0", - "decompress-targz": "^4.1.1", - "decompress-unzip": "^4.0.1", "deep-equal": "^2.0.1", "emotion": "^10.0.23", "expand-tilde": "^2.0.2", "flipper-doctor": "0.45.0", + "flipper-plugin-lib": "0.45.0", "fs-extra": "^8.0.1", "immer": "^6.0.0", "immutable": "^4.0.0-rc.12", "invariant": "^2.2.2", - "live-plugin-manager": "^0.14.0", "lodash": "^4.17.15", "npm-api": "^1.0.0", "open": "^7.0.0", diff --git a/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx b/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx index e17104fdf..a836aa219 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx @@ -44,8 +44,8 @@ import { provideSearchIndex, findPluginUpdates as _findPluginUpdates, UpdateResult, - installPluginFromNpm, } from '../../utils/pluginManager'; +import {installPluginFromNpm} from 'flipper-plugin-lib'; import {State as AppState} from '../../reducers'; import {connect} from 'react-redux'; import {Dispatch, Action} from 'redux'; diff --git a/desktop/app/src/chrome/plugin-manager/PluginPackageInstaller.tsx b/desktop/app/src/chrome/plugin-manager/PluginPackageInstaller.tsx index 3cba3d62d..9feb68e79 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginPackageInstaller.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginPackageInstaller.tsx @@ -19,7 +19,7 @@ import { import styled from '@emotion/styled'; import {default as FileSelector} from '../../ui/components/FileSelector'; import React, {useState} from 'react'; -import {installPluginFromFile} from '../../utils/pluginManager'; +import {installPluginFromFile} from 'flipper-plugin-lib'; const CenteredGlyph = styled(Glyph)({ margin: 'auto', diff --git a/desktop/app/src/utils/pluginManager.tsx b/desktop/app/src/utils/pluginManager.tsx index 78f7a432a..0f468c561 100644 --- a/desktop/app/src/utils/pluginManager.tsx +++ b/desktop/app/src/utils/pluginManager.tsx @@ -9,123 +9,17 @@ import path from 'path'; import fs from 'fs-extra'; -import {promisify} from 'util'; import {homedir} from 'os'; import {PluginMap, PluginDefinition} from '../reducers/pluginManager'; -import {PluginManager as PM} from 'live-plugin-manager'; import {default as algoliasearch, SearchIndex} from 'algoliasearch'; import NpmApi, {Package} from 'npm-api'; import semver from 'semver'; -import decompress from 'decompress'; -import decompressTargz from 'decompress-targz'; -import decompressUnzip from 'decompress-unzip'; -import tmp from 'tmp'; - -const getTmpDir = promisify(tmp.dir) as () => Promise; const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU'; const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e'; export const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty'); -function providePluginManager(): PM { - return new PM({ - ignoredDependencies: [/^flipper$/, /^react$/, /^react-dom$/, /^@types\//], - }); -} - -function providePluginManagerNoDependencies(): PM { - return new PM({ignoredDependencies: [/.*/]}); -} - -async function installPluginFromTempDir(pluginDir: string) { - const packageJSONPath = path.join(pluginDir, 'package.json'); - const packageJSON = JSON.parse( - (await fs.readFile(packageJSONPath)).toString(), - ); - const name = packageJSON.name; - - await fs.ensureDir(PLUGIN_DIR); - // create empty watchman config (required by metro's file watcher) - await fs.writeFile(path.join(PLUGIN_DIR, '.watchmanconfig'), '{}'); - const destinationDir = path.join(PLUGIN_DIR, name); - // Clean up existing destination files. - await fs.remove(destinationDir); - await fs.ensureDir(destinationDir); - - const isPreBundled = await fs.pathExists(path.join(pluginDir, 'dist')); - if (!isPreBundled) { - const pluginManager = providePluginManager(); - // install the plugin dependencies into node_modules - const nodeModulesDir = path.join(destinationDir, 'node_modules'); - pluginManager.options.pluginsPath = nodeModulesDir; - await pluginManager.installFromPath(pluginDir); - // live-plugin-manager also installs plugin itself into the target dir, it's better remove it - await fs.remove(path.join(nodeModulesDir, name)); - } - // copying plugin files into the destination folder - const pluginFiles = await fs.readdir(pluginDir); - await Promise.all( - pluginFiles - .filter((f) => f !== 'node_modules') - .map((f) => - fs.move(path.join(pluginDir, f), path.join(destinationDir, f)), - ), - ); -} - -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 installPluginFromNpm(name: string) { - const tmpDir = await getTmpDir(); - try { - await fs.ensureDir(tmpDir); - const plugManNoDep = providePluginManagerNoDependencies(); - plugManNoDep.options.pluginsPath = tmpDir; - await plugManNoDep.install(name); - const pluginTempDir = path.join(tmpDir, name); - await installPluginFromTempDir(pluginTempDir); - } finally { - if (await fs.pathExists(tmpDir)) { - await fs.remove(tmpDir); - } - } -} - -export async function installPluginFromFile(packagePath: string) { - const tmpDir = await getTmpDir(); - 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); - await installPluginFromTempDir(pluginDir); - } finally { - if (await fs.pathExists(tmpDir)) { - await fs.remove(tmpDir); - } - } -} - // TODO(T57014856): This should be private, too. export function provideSearchIndex(): SearchIndex { const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY); diff --git a/desktop/plugin-lib/package.json b/desktop/plugin-lib/package.json index e6c5f4917..20623d7f9 100644 --- a/desktop/plugin-lib/package.json +++ b/desktop/plugin-lib/package.json @@ -9,7 +9,12 @@ "license": "MIT", "bugs": "https://github.com/facebook/flipper/issues", "dependencies": { - "fs-extra": "^8.1.0" + "decompress": "^4.2.1", + "decompress-targz": "^4.1.1", + "decompress-unzip": "^4.0.1", + "fs-extra": "^8.1.0", + "live-plugin-manager": "^0.14.1", + "tmp": "^0.2.1" }, "devDependencies": { "@types/fs-extra": "^8.1.0", diff --git a/desktop/plugin-lib/src/index.ts b/desktop/plugin-lib/src/index.ts index 5f468a15f..d9e98e764 100644 --- a/desktop/plugin-lib/src/index.ts +++ b/desktop/plugin-lib/src/index.ts @@ -9,3 +9,4 @@ export {default as PluginDetails} from './PluginDetails'; export {default as getPluginDetails} from './getPluginDetails'; +export * from './pluginInstaller'; diff --git a/desktop/plugin-lib/src/pluginInstaller.tsx b/desktop/plugin-lib/src/pluginInstaller.tsx new file mode 100644 index 000000000..9ce3508a2 --- /dev/null +++ b/desktop/plugin-lib/src/pluginInstaller.tsx @@ -0,0 +1,120 @@ +/** + * 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 {promisify} from 'util'; +import {homedir} from 'os'; +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'; + +const getTmpDir = promisify(tmp.dir) as () => Promise; + +export const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty'); + +function providePluginManager(): PM { + return new PM({ + ignoredDependencies: [/^flipper$/, /^react$/, /^react-dom$/, /^@types\//], + }); +} + +function providePluginManagerNoDependencies(): PM { + return new PM({ignoredDependencies: [/.*/]}); +} + +async function installPluginFromTempDir(pluginDir: string) { + const packageJSONPath = path.join(pluginDir, 'package.json'); + const packageJSON = JSON.parse( + (await fs.readFile(packageJSONPath)).toString(), + ); + const name = packageJSON.name; + + await fs.ensureDir(PLUGIN_DIR); + // create empty watchman config (required by metro's file watcher) + await fs.writeFile(path.join(PLUGIN_DIR, '.watchmanconfig'), '{}'); + const destinationDir = path.join(PLUGIN_DIR, name); + // Clean up existing destination files. + await fs.remove(destinationDir); + await fs.ensureDir(destinationDir); + + const isPreBundled = await fs.pathExists(path.join(pluginDir, 'dist')); + if (!isPreBundled) { + const pluginManager = providePluginManager(); + // install the plugin dependencies into node_modules + const nodeModulesDir = path.join(destinationDir, 'node_modules'); + pluginManager.options.pluginsPath = nodeModulesDir; + await pluginManager.installFromPath(pluginDir); + // live-plugin-manager also installs plugin itself into the target dir, it's better remove it + await fs.remove(path.join(nodeModulesDir, name)); + } + // copying plugin files into the destination folder + const pluginFiles = await fs.readdir(pluginDir); + await Promise.all( + pluginFiles + .filter((f) => f !== 'node_modules') + .map((f) => + fs.move(path.join(pluginDir, f), path.join(destinationDir, f)), + ), + ); +} + +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 installPluginFromNpm(name: string) { + const tmpDir = await getTmpDir(); + try { + await fs.ensureDir(tmpDir); + const plugManNoDep = providePluginManagerNoDependencies(); + plugManNoDep.options.pluginsPath = tmpDir; + await plugManNoDep.install(name); + const pluginTempDir = path.join(tmpDir, name); + await installPluginFromTempDir(pluginTempDir); + } finally { + if (await fs.pathExists(tmpDir)) { + await fs.remove(tmpDir); + } + } +} + +export async function installPluginFromFile(packagePath: string) { + const tmpDir = await getTmpDir(); + 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); + await installPluginFromTempDir(pluginDir); + } finally { + if (await fs.pathExists(tmpDir)) { + await fs.remove(tmpDir); + } + } +} diff --git a/desktop/yarn.lock b/desktop/yarn.lock index bbebc988a..e3032b48e 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -1704,7 +1704,7 @@ widest-line "^2.0.1" wrap-ansi "^4.0.0" -"@oclif/plugin-help@^3": +"@oclif/plugin-help@^3", "@oclif/plugin-help@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.0.1.tgz#21919bc6bef58eb045b80312dbb1d671ee7bba51" integrity sha512-Q1OITeUBkkydPf6r5qX75KgE9capr1mNrfHtfD7gkVXmqoTndrbc++z4KfAYNf5nhTCY7N9l52sjbF6BrSGu9w== @@ -1719,21 +1719,6 @@ widest-line "^2.0.1" wrap-ansi "^4.0.0" -"@oclif/plugin-help@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.0.0.tgz#7d6433d74b0473a43797c6e0468b503470f23b50" - integrity sha512-mrV1O1VXy+ssW0kmIvFYkuEEPYZWKpyqydyHbKa316esAHatsZlrw6cRItf3TuKHTAqeGuXPctPV4mO2e21F9w== - dependencies: - "@oclif/command" "^1.5.20" - "@oclif/config" "^1.15.1" - chalk "^2.4.1" - indent-string "^4.0.0" - lodash.template "^4.4.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - widest-line "^2.0.1" - wrap-ansi "^4.0.0" - "@oclif/plugin-warn-if-update-available@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-1.7.0.tgz#5a72abe39ce0b831eb4ae81cb64eb4b9f3ea424a" @@ -1939,7 +1924,7 @@ dependencies: "@types/events" "*" -"@types/fs-extra@^8.0.0", "@types/fs-extra@^8.0.1", "@types/fs-extra@^8.1.0": +"@types/fs-extra@^8.0.0", "@types/fs-extra@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" integrity sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg== @@ -2070,10 +2055,10 @@ dependencies: "@types/node" "*" -"@types/node-fetch@^2.5.4": - version "2.5.5" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.5.tgz#cd264e20a81f4600a6c52864d38e7fef72485e92" - integrity sha512-IWwjsyYjGw+em3xTvWVQi5MgYKbRs0du57klfTaZkv/B24AEQ/p/IopNeqIYNy3EsfHOpg8ieQSDomPcsYMHpA== +"@types/node-fetch@^2.5.6": + version "2.5.7" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" + integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== dependencies: "@types/node" "*" form-data "^3.0.0" @@ -2279,10 +2264,12 @@ "@types/node" "*" "@types/rsocket-flowable" "*" -"@types/semver@^6.2.0": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" - integrity sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA== +"@types/semver@^7.1.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" + integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ== + dependencies: + "@types/node" "*" "@types/serve-static@*": version "1.13.3" @@ -3673,11 +3660,16 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chownr@^1.1.1, chownr@^1.1.3: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chromium-pickle-js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" @@ -4413,7 +4405,7 @@ decompress-unzip@^4.0.1: pify "^2.3.0" yauzl "^2.4.2" -decompress@^4.2.0: +decompress@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== @@ -8255,24 +8247,24 @@ linked-list@0.1.0: resolved "https://registry.yarnpkg.com/linked-list/-/linked-list-0.1.0.tgz#798b0ff97d1b92a4fd08480f55aea4e9d49d37bf" integrity sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78= -live-plugin-manager@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/live-plugin-manager/-/live-plugin-manager-0.14.0.tgz#234f5623e6d5768a8191fe9be009e1ec0fd7a2d8" - integrity sha512-wYf65O93USi8Oz2elo/TtFfq3+yYt7iz08RC3whSnlhKuobPBI2te7qzYJMmGBe3aKheamfqrN92LaPMdCAUfw== +live-plugin-manager@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/live-plugin-manager/-/live-plugin-manager-0.14.1.tgz#34b6bb8f3d2062ec2557c857ca028fea9dc2b6db" + integrity sha512-bRFh5xn1PEYrwxoOhQ4k8gsh8eh4cRMbXmV/YefV4Xs2E6wuXLF+e51gsXjyim7DquEWsiFuoKK+RSQ387PuHA== dependencies: "@types/debug" "^4.1.5" - "@types/fs-extra" "^8.0.1" + "@types/fs-extra" "^8.1.0" "@types/lockfile" "^1.0.1" - "@types/node-fetch" "^2.5.4" - "@types/semver" "^6.2.0" + "@types/node-fetch" "^2.5.6" + "@types/semver" "^7.1.0" "@types/tar" "^4.0.3" "@types/url-join" "4.0.0" debug "^4.1.1" - fs-extra "^8.1.0" + fs-extra "^9.0.0" lockfile "^1.0.4" node-fetch "^2.6.0" - semver "^6.3.0" - tar "^5.0.5" + semver "^7.3.2" + tar "^6.0.1" url-join "^4.0.1" load-json-file@^1.0.0: @@ -8953,7 +8945,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@1.x, mkdirp@^1.0.4: +mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -11959,16 +11951,16 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93" - integrity sha512-MNIgJddrV2TkuwChwcSNds/5E9VijOiw7kAc1y5hTNJoLDSuIyid2QtLYiCYNnICebpuvjhPQZsXwUL0O3l7OQ== +tar@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" + integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== dependencies: - chownr "^1.1.3" + chownr "^2.0.0" fs-minipass "^2.0.0" minipass "^3.0.0" minizlib "^2.1.0" - mkdirp "^0.5.0" + mkdirp "^1.0.3" yallist "^4.0.0" temp-file@^3.3.6: