diff --git a/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts b/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts index 47242a669..0d0a517c9 100644 --- a/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts +++ b/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts @@ -29,7 +29,7 @@ test('getPluginDetailsV1', async () => { "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", - "main": "dist/index.js", + "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", "specVersion": 1, diff --git a/desktop/pkg-lib/src/getPluginDetails.ts b/desktop/pkg-lib/src/getPluginDetails.ts index b90bffcc7..ed3a4a9d2 100644 --- a/desktop/pkg-lib/src/getPluginDetails.ts +++ b/desktop/pkg-lib/src/getPluginDetails.ts @@ -43,7 +43,7 @@ async function getPluginDetailsV1( dir: pluginDir, name: packageJson.name, version: packageJson.version, - main: 'dist/index.js', + main: 'dist/bundle.js', source: packageJson.main, id: packageJson.name, gatekeeper: packageJson.gatekeeper, diff --git a/desktop/pkg/README.md b/desktop/pkg/README.md index a7e022a2c..e801b023b 100644 --- a/desktop/pkg/README.md +++ b/desktop/pkg/README.md @@ -28,6 +28,7 @@ USAGE * [`flipper-pkg help [COMMAND]`](#flipper-pkg-help-command) * [`flipper-pkg init [DIRECTORY]`](#flipper-pkg-init-directory) * [`flipper-pkg lint [DIRECTORY]`](#flipper-pkg-lint-directory) +* [`flipper-pkg migrate [DIRECTORY]`](#flipper-pkg-migrate-directory) * [`flipper-pkg pack [DIRECTORY]`](#flipper-pkg-pack-directory) ## `flipper-pkg bundle [DIRECTORY]` @@ -66,15 +67,15 @@ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3 ## `flipper-pkg init [DIRECTORY]` -initializes Flipper desktop plugin template in the provided directory +initializes a Flipper desktop plugin template in the provided directory ``` USAGE $ flipper-pkg init [DIRECTORY] ARGUMENTS - DIRECTORY [default: .] Path to directory where plugin package template should be initialized. Defaults to the current - working directory. + DIRECTORY [default: .] Path to the directory where the plugin package template should be initialized. Defaults to the + current working directory. EXAMPLE $ flipper-pkg init path/to/plugin @@ -99,6 +100,27 @@ EXAMPLE _See code: [src/commands/lint.ts](https://github.com/facebook/flipper/blob/v0.39.0/src/commands/lint.ts)_ +## `flipper-pkg migrate [DIRECTORY]` + +migrates a Flipper desktop plugin to the latest version of specification + +``` +USAGE + $ flipper-pkg migrate [DIRECTORY] + +ARGUMENTS + DIRECTORY [default: .] Path to the plugin directory. Defaults to the current working directory. + +OPTIONS + --no-dependencies Do not add or change package dependencies during migration. + --no-scripts Do not add or change package scripts during migration. + +EXAMPLE + $ flipper-pkg migrate path/to/plugin +``` + +_See code: [src/commands/migrate.ts](https://github.com/facebook/flipper/blob/v0.39.0/src/commands/migrate.ts)_ + ## `flipper-pkg pack [DIRECTORY]` packs a plugin folder into a distributable archive diff --git a/desktop/pkg/package.json b/desktop/pkg/package.json index 2d496a8db..2b2b51e8b 100644 --- a/desktop/pkg/package.json +++ b/desktop/pkg/package.json @@ -23,6 +23,7 @@ "flipper-pkg-lib": "0.39.0", "fs-extra": "^8.1.0", "inquirer": "^7.1.0", + "lodash": "^4.17.15", "recursive-readdir": "^2.2.2" }, "devDependencies": { diff --git a/desktop/pkg/src/__tests__/runMigrate.node.ts b/desktop/pkg/src/__tests__/runMigrate.node.ts new file mode 100644 index 000000000..1611884cc --- /dev/null +++ b/desktop/pkg/src/__tests__/runMigrate.node.ts @@ -0,0 +1,144 @@ +/** + * 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 runMigrate from '../utils/runMigrate'; +import fs from 'fs-extra'; + +const packageJsonV1 = { + name: 'Fresco', + version: '1.0.0', + main: 'index.tsx', + license: 'MIT', + keywords: ['images'], + dependencies: { + flipper: 'latest', + }, + scripts: { + prepack: 'yarn reset && yarn build', + }, + title: 'Images', + icon: 'profile', + bugs: { + email: 'example@test.com', + }, +}; + +const packageJsonV2 = { + $schema: 'https://fbflipper.com/schemas/plugin-package/v2.json', + name: 'flipper-plugin-network', + id: 'Network', + flipperBundlerEntry: 'index.tsx', + main: 'dist/index.js', + title: 'Network', + description: + 'Use the Network inspector to inspect outgoing network traffic in your apps.', + icon: 'internet', + version: '1.0.0', + license: 'MIT', + keywords: ['network', 'flipper-plugin'], + scripts: { + prepack: 'yarn reset && yarn build', + }, + bugs: { + email: 'example@test.com', + url: 'https://github.com/facebook/flipper', + }, +}; + +let convertedPackageJsonString: string | undefined; + +beforeEach(() => { + jest.mock('fs-extra', () => jest.fn()); + fs.pathExists = jest.fn().mockResolvedValue(true); + fs.pathExistsSync = jest.fn().mockReturnValue(true); + fs.readJson = jest.fn().mockResolvedValue(packageJsonV1); + fs.readFile = jest + .fn() + .mockResolvedValue(new Buffer(JSON.stringify(packageJsonV1))); + convertedPackageJsonString = undefined; + fs.writeFile = jest.fn().mockImplementation(async (_path, content) => { + convertedPackageJsonString = content; + }); +}); + +test('converts package.json and adds dependencies', async () => { + const error = await runMigrate('dir'); + expect(error).toBeUndefined(); + expect(convertedPackageJsonString).toMatchInlineSnapshot(` + "{ + \\"$schema\\": \\"https://fbflipper.com/schemas/plugin-package/v2.json\\", + \\"name\\": \\"flipper-plugin-fresco\\", + \\"id\\": \\"Fresco\\", + \\"version\\": \\"1.0.0\\", + \\"main\\": \\"dist/bundle.js\\", + \\"flipperBundlerEntry\\": \\"index.tsx\\", + \\"license\\": \\"MIT\\", + \\"keywords\\": [ + \\"flipper-plugin\\", + \\"images\\" + ], + \\"peerDependencies\\": { + \\"flipper\\": \\"latest\\" + }, + \\"devDependencies\\": { + \\"flipper\\": \\"latest\\", + \\"flipper-pkg\\": \\"latest\\" + }, + \\"scripts\\": { + \\"prepack\\": \\"yarn reset && yarn build && flipper-pkg lint && flipper-pkg bundle\\" + }, + \\"title\\": \\"Images\\", + \\"icon\\": \\"profile\\", + \\"bugs\\": { + \\"email\\": \\"example@test.com\\" + } + }" + `); +}); + +test('converts package.json without changing dependencies', async () => { + const error = await runMigrate('dir', {noDependencies: true}); + expect(error).toBeUndefined(); + expect(convertedPackageJsonString).toMatchInlineSnapshot(` + "{ + \\"$schema\\": \\"https://fbflipper.com/schemas/plugin-package/v2.json\\", + \\"name\\": \\"flipper-plugin-fresco\\", + \\"id\\": \\"Fresco\\", + \\"version\\": \\"1.0.0\\", + \\"main\\": \\"dist/bundle.js\\", + \\"flipperBundlerEntry\\": \\"index.tsx\\", + \\"license\\": \\"MIT\\", + \\"keywords\\": [ + \\"flipper-plugin\\", + \\"images\\" + ], + \\"dependencies\\": { + \\"flipper\\": \\"latest\\" + }, + \\"scripts\\": { + \\"prepack\\": \\"yarn reset && yarn build && flipper-pkg lint && flipper-pkg bundle\\" + }, + \\"title\\": \\"Images\\", + \\"icon\\": \\"profile\\", + \\"bugs\\": { + \\"email\\": \\"example@test.com\\" + } + }" + `); +}); + +test('does not migrate already migrated packages', async () => { + fs.readJson = jest.fn().mockResolvedValue(packageJsonV2); + fs.readFile = jest + .fn() + .mockResolvedValue(new Buffer(JSON.stringify(packageJsonV2))); + const error = await runMigrate('dir'); + expect(error).toBeUndefined(); + expect(convertedPackageJsonString).toBeUndefined(); +}); diff --git a/desktop/pkg/src/commands/migrate.ts b/desktop/pkg/src/commands/migrate.ts new file mode 100644 index 000000000..aa2f7d6ae --- /dev/null +++ b/desktop/pkg/src/commands/migrate.ts @@ -0,0 +1,53 @@ +/** + * 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 {Command, flags} from '@oclif/command'; +import {args} from '@oclif/parser'; +import runMigrate from '../utils/runMigrate'; +import path from 'path'; + +export default class Migrate extends Command { + public static description = + 'migrates a Flipper desktop plugin to the latest version of specification'; + + public static examples = [`$ flipper-pkg migrate path/to/plugin`]; + + public static flags = { + 'no-dependencies': flags.boolean({ + description: + 'Do not add or change package dependencies during migration.', + default: false, + }), + 'no-scripts': flags.boolean({ + description: 'Do not add or change package scripts during migration.', + default: false, + }), + }; + + public static args: args.IArg[] = [ + { + name: 'directory', + required: false, + default: '.', + description: + 'Path to the plugin directory. Defaults to the current working directory.', + }, + ]; + + public async run() { + const {args, flags} = this.parse(Migrate); + const dir: string = path.resolve(process.cwd(), args.directory); + const noDependencies = flags['no-dependencies']; + const noScripts = flags['no-scripts']; + const error = await runMigrate(dir, {noDependencies, noScripts}); + if (error) { + this.error(error); + } + } +} diff --git a/desktop/pkg/src/utils/runMigrate.ts b/desktop/pkg/src/utils/runMigrate.ts new file mode 100644 index 000000000..419264af8 --- /dev/null +++ b/desktop/pkg/src/utils/runMigrate.ts @@ -0,0 +1,129 @@ +/** + * 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 {getPluginDetails} from 'flipper-pkg-lib'; +import {kebabCase} from 'lodash'; + +export default async function ( + dir: string, + options: { + noDependencies?: boolean; + noScripts?: boolean; + } = {}, +): Promise { + const {noDependencies, noScripts} = Object.assign( + { + noDependencies: false, + noScripts: false, + }, + options, + ); + if (!(await fs.pathExists(dir))) { + return `Directory not found: ${dir}`; + } + const packageJsonPath = path.join(dir, 'package.json'); + if (!(await fs.pathExists(packageJsonPath))) { + return `package.json not found: ${packageJsonPath}`; + } + console.log(`⚙️ Migrating Flipper plugin package in ${dir}`); + const packageJsonString = (await fs.readFile(packageJsonPath)).toString(); + const packageJson = JSON.parse(packageJsonString); + const pluginDetails = await getPluginDetails(dir, packageJson); + if (pluginDetails.specVersion === 2) { + console.log( + `✅ Plugin is already defined according to the latest specification version.`, + ); + return; + } + const name = pluginDetails.name.startsWith('flipper-plugin-') + ? pluginDetails.name + : `flipper-plugin-${kebabCase(pluginDetails.name)}`; + const keys = Object.keys(packageJson); + packageJson.name = name; + packageJson.main = pluginDetails.main; + if (!packageJson.flipperBundlerEntry) { + const index = keys.indexOf('main'); + keys.splice(index + 1, 0, 'flipperBundlerEntry'); + } + packageJson.flipperBundlerEntry = pluginDetails.source; + if (!packageJson.id) { + const index = keys.indexOf('name'); + keys.splice(index + 1, 0, 'id'); + } + packageJson.id = pluginDetails.id; + if (!packageJson.$schema) { + keys.unshift('$schema'); + } + packageJson.$schema = 'https://fbflipper.com/schemas/plugin-package/v2.json'; + if (!packageJson.keywords) { + keys.push('keywords'); + } + if ( + !packageJson.keywords || + !packageJson.keywords.includes('flipper-plugin') + ) { + packageJson.keywords = ['flipper-plugin', ...(packageJson.keywords || [])]; + } + + if (!noDependencies) { + const dependenciesFieldIndex = keys.indexOf('dependencies'); + if (packageJson.dependencies && packageJson.dependencies.flipper) { + delete packageJson.dependencies.flipper; + } + if (!packageJson.peerDependencies) { + if (dependenciesFieldIndex === -1) { + keys.push('peerDependencies'); + } else { + if (Object.keys(packageJson.dependencies).length === 0) { + // If no other dependencies except 'flipper' then we need to remove 'dependencies' and add 'peerDependencies' instead + keys.splice(dependenciesFieldIndex, 1, 'peerDependencies'); + } else { + // If there are other dependencies except 'flipper', then + keys.splice(dependenciesFieldIndex + 1, 0, 'peerDependencies'); + } + } + } + packageJson.peerDependencies = { + ...packageJson.peerDependencies, + flipper: 'latest', + }; + if (!packageJson.devDependencies) { + const peerDependenciesFieldIndex = keys.indexOf('peerDependencies'); + keys.splice(peerDependenciesFieldIndex + 1, 0, 'devDependencies'); + } + packageJson.devDependencies = { + ...packageJson.devDependencies, + flipper: 'latest', + 'flipper-pkg': 'latest', + }; + } + + if (!noScripts) { + if (!packageJson.scripts) { + keys.push('scripts'); + } + packageJson.scripts = { + ...packageJson.scripts, + prepack: + (packageJson.scripts?.prepack + ? packageJson.scripts!.prepack! + ' && ' + : '') + 'flipper-pkg lint && flipper-pkg bundle', + }; + } + + const newPackageJson = keys.reduce((result, key) => { + result[key] = packageJson[key]; + return result; + }, {}); + const newPackageJsonString = JSON.stringify(newPackageJson, undefined, 2); + await fs.writeFile(packageJsonPath, newPackageJsonString); + console.log(`✅ Plugin migrated to the latest specification version 2.`); +} diff --git a/docs/extending/jssetup.mdx b/docs/extending/jssetup.mdx index ad3335700..17114c17b 100644 --- a/docs/extending/jssetup.mdx +++ b/docs/extending/jssetup.mdx @@ -132,6 +132,14 @@ Flipper has [tooling for transpiling and bundling](#transpiling-and-bundling) wh If you need any dependencies in your plugin, you can install them using `yarn add`. +## Migration to the new Plugin Specification + +Flipper plugins are defined according to the specification. As with any specification, it is evolving, so new versions of it can be released. Currently Flipper supports plugins defined using version 2 of specification which is described in this page. Previous version of specification is being deprecated, and we encourage all the plugins still using it to migrate. + +The main difference of version 2 is that plugins are transpiled and bundled before packaging, while in version 1 this was done in run-time on plugin installation. There are no plugin API changes, so only the `package.json` changes are required to migrate. + +The easiest way for migration is using of command `flipper-pkg migrate`. It will automatically migrate your plugin definition to the latest version. + ## Development Build A Flipper development build should be used for plugin debugging. It is also used for Flipper core development and provides the following features: