diff --git a/desktop/flipper-plugin/tsconfig.json b/desktop/flipper-plugin/tsconfig.json index ae65393b7..e0c4d7bd8 100644 --- a/desktop/flipper-plugin/tsconfig.json +++ b/desktop/flipper-plugin/tsconfig.json @@ -4,7 +4,16 @@ "outDir": "lib", "rootDir": "src" }, - "references": [{"path": "../plugin-lib"}], - "include": ["src"], - "exclude": ["node_modules"] + "references": [ + { + "path": "../plugin-lib" + } + ], + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "**/__tests__/*" + ] } diff --git a/desktop/pkg/schemas/plugin-package-v2.json b/desktop/pkg/schemas/plugin-package-v2.json index 7569998ca..c70cca6f6 100644 --- a/desktop/pkg/schemas/plugin-package-v2.json +++ b/desktop/pkg/schemas/plugin-package-v2.json @@ -41,7 +41,7 @@ "errorMessage": "should contain keyword \"flipper-plugin\"" }, "pluginType": { - "description": "Type of the plugin - client or device. If omitted, \"client\" type is used by default.", + "description": "Type of the plugin - \"client\" if the plugin connects to a specific client plugin running in a mobile app, or \"device\" if it connects to devices. If omitted, \"client\" type is assumed by default.", "type": "string", "enum": ["client", "device"] }, @@ -84,6 +84,6 @@ "id", "main", "flipperBundlerEntry", - "keywords" + "keywords" ] } diff --git a/desktop/pkg/src/__tests__/runInit.node.ts b/desktop/pkg/src/__tests__/runInit.node.ts index 3ce8533ce..b42da7b2b 100644 --- a/desktop/pkg/src/__tests__/runInit.node.ts +++ b/desktop/pkg/src/__tests__/runInit.node.ts @@ -37,8 +37,14 @@ afterEach(() => { // fs.writeFile.mockRestore(); }); -test('It generates the correct files', async () => { - await initTemplate('my weird Package %name. etc', 'Nice title', '/dev/null'); +test('It generates the correct files for client plugin', async () => { + await initTemplate( + 'my weird Package %name. etc', + 'Nice title', + 'client', + undefined, + '/dev/null', + ); expect(files).toMatchInlineSnapshot(` Object { "/dev/null/.gitignore": "node_modules @@ -57,6 +63,7 @@ test('It generates the correct files', async () => { \\"name\\": \\"flipper-plugin-my-weird-package-name-etc\\", \\"id\\": \\"my weird Package %name. etc\\", \\"version\\": \\"1.0.0\\", + \\"pluginType\\": \\"client\\", \\"main\\": \\"dist/bundle.js\\", \\"flipperBundlerEntry\\": \\"src/index.tsx\\", \\"license\\": \\"MIT\\", @@ -206,3 +213,202 @@ test('It generates the correct files', async () => { } `); }); + +test('It generates the correct files for device plugin', async () => { + await initTemplate( + 'my weird Package %name. etc', + 'Nice title', + 'device', + ['iOS', 'Android'], + '/dev/null', + ); + expect(files).toMatchInlineSnapshot(` + Object { + "/dev/null/.gitignore": "node_modules + dist/ + ", + "/dev/null/babel.config.js": "module.exports = { + presets: [ + '@babel/preset-typescript', + '@babel/preset-react', + ['@babel/preset-env', {targets: {node: 'current'}}] + ], + }; + ", + "/dev/null/package.json": "{ + \\"$schema\\": \\"https://fbflipper.com/schemas/plugin-package/v2.json\\", + \\"name\\": \\"flipper-plugin-my-weird-package-name-etc\\", + \\"id\\": \\"my weird Package %name. etc\\", + \\"version\\": \\"1.0.0\\", + \\"pluginType\\": \\"device\\", + \\"supportedDevices\\": [{\\"os\\":\\"iOS\\"},{\\"os\\":\\"Android\\"}], + \\"main\\": \\"dist/bundle.js\\", + \\"flipperBundlerEntry\\": \\"src/index.tsx\\", + \\"license\\": \\"MIT\\", + \\"keywords\\": [ + \\"flipper-plugin\\" + ], + \\"icon\\": \\"apps\\", + \\"title\\": \\"Nice title\\", + \\"scripts\\": { + \\"lint\\": \\"flipper-pkg lint\\", + \\"prepack\\": \\"flipper-pkg lint && flipper-pkg bundle\\", + \\"build\\": \\"flipper-pkg bundle\\", + \\"watch\\": \\"flipper-pkg bundle --watch\\", + \\"test\\": \\"jest --no-watchman\\" + }, + \\"peerDependencies\\": { + \\"flipper\\": \\"latest\\", + \\"flipper-plugin\\": \\"latest\\", + \\"antd\\": \\"latest\\" + }, + \\"devDependencies\\": { + \\"@babel/preset-react\\": \\"latest\\", + \\"@babel/preset-typescript\\": \\"latest\\", + \\"@testing-library/react\\": \\"latest\\", + \\"@types/jest\\": \\"latest\\", + \\"@types/react\\": \\"latest\\", + \\"@types/react-dom\\": \\"latest\\", + \\"antd\\": \\"latest\\", + \\"flipper\\": \\"latest\\", + \\"flipper-plugin\\": \\"latest\\", + \\"flipper-pkg\\": \\"latest\\", + \\"jest\\": \\"latest\\", + \\"typescript\\": \\"latest\\" + } + } + ", + "/dev/null/src/__tests__/test.spec.tsx": "import {TestUtils} from 'flipper-plugin'; + import * as Plugin from '..'; + + // Read more: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic + // API: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic + test('It can store data', () => { + const {instance, sendLogEntry} = TestUtils.startDevicePlugin(Plugin); + + expect(instance.data.get()).toEqual([]); + + sendLogEntry({ + date: new Date(1611854112859), + message: 'test1', + pid: 0, + tag: 'test', + tid: 1, + type: 'error', + app: 'X', + }); + sendLogEntry({ + date: new Date(1611854117859), + message: 'test2', + pid: 2, + tag: 'test', + tid: 3, + type: 'warn', + app: 'Y', + }); + + expect(instance.data.get()).toMatchInlineSnapshot(\` + Array [ + \\"test1\\", + \\"test2\\", + ] + \`); + }); + + // Read more: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic + // API: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic + test('It can render data', async () => { + const {instance, renderer, sendLogEntry} = TestUtils.renderDevicePlugin( + Plugin, + ); + + expect(instance.data.get()).toEqual([]); + + sendLogEntry({ + date: new Date(1611854112859), + message: 'test1', + pid: 0, + tag: 'test', + tid: 1, + type: 'error', + app: 'X', + }); + sendLogEntry({ + date: new Date(1611854117859), + message: 'test2', + pid: 2, + tag: 'test', + tid: 3, + type: 'warn', + app: 'Y', + }); + + expect(await renderer.findByTestId('0')).not.toBeNull(); + expect(await renderer.findByTestId('1')).toMatchInlineSnapshot(); + }); + ", + "/dev/null/src/index.tsx": "import React from 'react'; + import { + DevicePluginClient, + usePlugin, + createState, + useValue, + Layout, + } from 'flipper-plugin'; + + // Read more: https://fbflipper.com/docs/tutorial/js-custom#creating-a-first-plugin + // API: https://fbflipper.com/docs/extending/flipper-plugin#pluginclient + export function devicePlugin(client: DevicePluginClient) { + const data = createState([]); + + client.device.onLogEntry((entry) => { + data.update((draft) => { + draft.push(entry.message); + }); + }); + + client.addMenuEntry({ + action: 'clear', + handler: async () => { + data.set([]); + }, + }); + + return {data}; + } + + // Read more: https://fbflipper.com/docs/tutorial/js-custom#building-a-user-interface-for-the-plugin + // API: https://fbflipper.com/docs/extending/flipper-plugin#react-hooks + export function Component() { + const instance = usePlugin(devicePlugin); + const data = useValue(instance.data); + + return ( + + {Object.entries(data).map(([id, d]) => ( +
+              {JSON.stringify(d)}
+            
+ ))} +
+ ); + } + ", + "/dev/null/tsconfig.json": "{ + \\"compilerOptions\\": { + \\"target\\": \\"ES2017\\", + \\"module\\": \\"ES6\\", + \\"jsx\\": \\"react\\", + \\"sourceMap\\": true, + \\"noEmit\\": true, + \\"strict\\": true, + \\"moduleResolution\\": \\"node\\", + \\"esModuleInterop\\": true, + \\"forceConsistentCasingInFileNames\\": true + }, + \\"files\\": [\\"src/index.tsx\\"] + } + ", + } + `); +}); diff --git a/desktop/pkg/src/commands/init.ts b/desktop/pkg/src/commands/init.ts index bdb3b9a26..83ece650e 100644 --- a/desktop/pkg/src/commands/init.ts +++ b/desktop/pkg/src/commands/init.ts @@ -16,10 +16,24 @@ import recursiveReaddirImport from 'recursive-readdir'; import {promisify} from 'util'; import inquirer from 'inquirer'; import {homedir} from 'os'; +import {PluginType} from 'flipper-plugin-lib'; const recursiveReaddir = promisify(recursiveReaddirImport); -const templateDir = path.resolve(__dirname, '..', '..', 'templates', 'plugin'); +const pluginTemplateDir = path.resolve( + __dirname, + '..', + '..', + 'templates', + 'plugin', +); +const devicePluginTemplateDir = path.resolve( + __dirname, + '..', + '..', + 'templates', + 'device-plugin', +); const templateExt = '.template'; export default class Init extends Command { @@ -43,6 +57,18 @@ export default class Init extends Command { const pluginDirectory: string = path.resolve(process.cwd(), args.directory); await verifyFlipperSearchPath(pluginDirectory); + const pluginTypeQuestion: inquirer.QuestionCollection = [ + { + type: 'list', + name: 'pluginType', + choices: ['client', 'device'], + message: + 'Plugin Type ("client" if the plugin will work with a mobile app, "device" if the plugin will work with a mobile device):', + default: 'client', + }, + ]; + const pluginType: PluginType = (await inquirer.prompt(pluginTypeQuestion)) + .pluginType; const idQuestion: inquirer.QuestionCollection = [ { type: 'input', @@ -61,6 +87,24 @@ export default class Init extends Command { }, ]; const title: string = (await inquirer.prompt(titleQuestion)).title; + + let supportedDevices: string[] | undefined; + + if (pluginType === 'device') { + const supportedDevicesQuestion: inquirer.QuestionCollection = [ + { + type: 'checkbox', + name: 'supportedDevices', + choices: ['iOS', 'Android', 'Metro'], + message: + 'Supported Devices (iOS, Android or Metro (React Native bundler)):', + default: ['iOS', 'Android'], + }, + ]; + supportedDevices = (await inquirer.prompt(supportedDevicesQuestion)) + .supportedDevices; + } + const packageName = getPackageNameFromId(id); const outputDirectory = path.join(pluginDirectory, packageName); @@ -72,7 +116,13 @@ export default class Init extends Command { `⚙️ Initializing Flipper desktop template in ${outputDirectory}`, ); await fs.ensureDir(outputDirectory); - await initTemplate(id, title, outputDirectory); + await initTemplate( + id, + title, + pluginType, + supportedDevices, + outputDirectory, + ); console.log(`⚙️ Installing dependencies`); spawnSync('yarn', ['install'], {cwd: outputDirectory, stdio: [0, 1, 2]}); @@ -93,9 +143,13 @@ function getPackageNameFromId(id: string): string { export async function initTemplate( id: string, title: string, + pluginType: PluginType, + supportedDevices: string[] | undefined, outputDirectory: string, ) { const packageName = getPackageNameFromId(id); + const templateDir = + pluginType === 'device' ? devicePluginTemplateDir : pluginTemplateDir; const templateItems = await recursiveReaddir(templateDir); for (const item of templateItems) { @@ -115,6 +169,16 @@ export async function initTemplate( .toString() .replace('{{id}}', id) .replace('{{title}}', title) + .replace( + '{{supported_devices}}', + JSON.stringify( + supportedDevices + ? supportedDevices.map((d) => ({ + os: d, + })) + : [], + ), + ) .replace('{{package_name}}', packageName); await fs.writeFile(newFile, content); } diff --git a/desktop/pkg/templates/device-plugin/.gitignore.template b/desktop/pkg/templates/device-plugin/.gitignore.template new file mode 100644 index 000000000..44d646d58 --- /dev/null +++ b/desktop/pkg/templates/device-plugin/.gitignore.template @@ -0,0 +1,2 @@ +node_modules +dist/ diff --git a/desktop/pkg/templates/device-plugin/babel.config.js.template b/desktop/pkg/templates/device-plugin/babel.config.js.template new file mode 100644 index 000000000..9bdd46258 --- /dev/null +++ b/desktop/pkg/templates/device-plugin/babel.config.js.template @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + '@babel/preset-typescript', + '@babel/preset-react', + ['@babel/preset-env', {targets: {node: 'current'}}] + ], +}; diff --git a/desktop/pkg/templates/device-plugin/package.json.template b/desktop/pkg/templates/device-plugin/package.json.template new file mode 100644 index 000000000..6262b9380 --- /dev/null +++ b/desktop/pkg/templates/device-plugin/package.json.template @@ -0,0 +1,42 @@ +{ + "$schema": "https://fbflipper.com/schemas/plugin-package/v2.json", + "name": "{{package_name}}", + "id": "{{id}}", + "version": "1.0.0", + "pluginType": "device", + "supportedDevices": {{supported_devices}}, + "main": "dist/bundle.js", + "flipperBundlerEntry": "src/index.tsx", + "license": "MIT", + "keywords": [ + "flipper-plugin" + ], + "icon": "apps", + "title": "{{title}}", + "scripts": { + "lint": "flipper-pkg lint", + "prepack": "flipper-pkg lint && flipper-pkg bundle", + "build": "flipper-pkg bundle", + "watch": "flipper-pkg bundle --watch", + "test": "jest --no-watchman" + }, + "peerDependencies": { + "flipper": "latest", + "flipper-plugin": "latest", + "antd": "latest" + }, + "devDependencies": { + "@babel/preset-react": "latest", + "@babel/preset-typescript": "latest", + "@testing-library/react": "latest", + "@types/jest": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "antd": "latest", + "flipper": "latest", + "flipper-plugin": "latest", + "flipper-pkg": "latest", + "jest": "latest", + "typescript": "latest" + } +} diff --git a/desktop/pkg/templates/device-plugin/src/__tests__/test.spec.tsx.template b/desktop/pkg/templates/device-plugin/src/__tests__/test.spec.tsx.template new file mode 100644 index 000000000..78b9e90ee --- /dev/null +++ b/desktop/pkg/templates/device-plugin/src/__tests__/test.spec.tsx.template @@ -0,0 +1,68 @@ +import {TestUtils} from 'flipper-plugin'; +import * as Plugin from '..'; + +// Read more: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic +// API: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic +test('It can store data', () => { + const {instance, sendLogEntry} = TestUtils.startDevicePlugin(Plugin); + + expect(instance.data.get()).toEqual([]); + + sendLogEntry({ + date: new Date(1611854112859), + message: 'test1', + pid: 0, + tag: 'test', + tid: 1, + type: 'error', + app: 'X', + }); + sendLogEntry({ + date: new Date(1611854117859), + message: 'test2', + pid: 2, + tag: 'test', + tid: 3, + type: 'warn', + app: 'Y', + }); + + expect(instance.data.get()).toMatchInlineSnapshot(` + Array [ + "test1", + "test2", + ] + `); +}); + +// Read more: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic +// API: https://fbflipper.com/docs/tutorial/js-custom#testing-plugin-logic +test('It can render data', async () => { + const {instance, renderer, sendLogEntry} = TestUtils.renderDevicePlugin( + Plugin, + ); + + expect(instance.data.get()).toEqual([]); + + sendLogEntry({ + date: new Date(1611854112859), + message: 'test1', + pid: 0, + tag: 'test', + tid: 1, + type: 'error', + app: 'X', + }); + sendLogEntry({ + date: new Date(1611854117859), + message: 'test2', + pid: 2, + tag: 'test', + tid: 3, + type: 'warn', + app: 'Y', + }); + + expect(await renderer.findByTestId('0')).not.toBeNull(); + expect(await renderer.findByTestId('1')).toMatchInlineSnapshot(); +}); diff --git a/desktop/pkg/templates/device-plugin/src/index.tsx.template b/desktop/pkg/templates/device-plugin/src/index.tsx.template new file mode 100644 index 000000000..cc934cade --- /dev/null +++ b/desktop/pkg/templates/device-plugin/src/index.tsx.template @@ -0,0 +1,46 @@ +import React from 'react'; +import { + DevicePluginClient, + usePlugin, + createState, + useValue, + Layout, +} from 'flipper-plugin'; + +// Read more: https://fbflipper.com/docs/tutorial/js-custom#creating-a-first-plugin +// API: https://fbflipper.com/docs/extending/flipper-plugin#pluginclient +export function devicePlugin(client: DevicePluginClient) { + const data = createState([]); + + client.device.onLogEntry((entry) => { + data.update((draft) => { + draft.push(entry.message); + }); + }); + + client.addMenuEntry({ + action: 'clear', + handler: async () => { + data.set([]); + }, + }); + + return {data}; +} + +// Read more: https://fbflipper.com/docs/tutorial/js-custom#building-a-user-interface-for-the-plugin +// API: https://fbflipper.com/docs/extending/flipper-plugin#react-hooks +export function Component() { + const instance = usePlugin(devicePlugin); + const data = useValue(instance.data); + + return ( + + {Object.entries(data).map(([id, d]) => ( +
+          {JSON.stringify(d)}
+        
+ ))} +
+ ); +} diff --git a/desktop/pkg/templates/device-plugin/tsconfig.json.template b/desktop/pkg/templates/device-plugin/tsconfig.json.template new file mode 100644 index 000000000..fd08049ec --- /dev/null +++ b/desktop/pkg/templates/device-plugin/tsconfig.json.template @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ES6", + "jsx": "react", + "sourceMap": true, + "noEmit": true, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "files": ["src/index.tsx"] +} diff --git a/desktop/pkg/templates/plugin/package.json.template b/desktop/pkg/templates/plugin/package.json.template index c81fc8910..71234fcf9 100644 --- a/desktop/pkg/templates/plugin/package.json.template +++ b/desktop/pkg/templates/plugin/package.json.template @@ -3,6 +3,7 @@ "name": "{{package_name}}", "id": "{{id}}", "version": "1.0.0", + "pluginType": "client", "main": "dist/bundle.js", "flipperBundlerEntry": "src/index.tsx", "license": "MIT",