Summary: Moved all types related to plugin descriptions from plugin-lib (which handles downloads and such) to flipper-common. The goal of that is to remove all plugin-lib usage from ui-core to server-core, so that the UI itself doesn't do any file operations anymore related to plugins. That will be done in next diffs, this just moves types but no code. Reviewed By: nikoant, aigoncharov Differential Revision: D32665064 fbshipit-source-id: 86d908e7264569b0229b09290a891171876c8e00
246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
/**
|
|
* 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} from '@oclif/command';
|
|
import {args} from '@oclif/parser';
|
|
import path from 'path';
|
|
import fs from 'fs-extra';
|
|
import {spawnSync} from 'child_process';
|
|
import recursiveReaddirImport from 'recursive-readdir';
|
|
import {promisify} from 'util';
|
|
import inquirer from 'inquirer';
|
|
import {homedir} from 'os';
|
|
// only type imported
|
|
// eslint-disable-next-line
|
|
import type {PluginType} from 'flipper-common';
|
|
|
|
const recursiveReaddir = promisify<string, string[]>(recursiveReaddirImport);
|
|
|
|
const pluginTemplateDir = path.resolve(
|
|
__dirname,
|
|
'..',
|
|
'..',
|
|
'templates',
|
|
'plugin',
|
|
);
|
|
const devicePluginTemplateDir = path.resolve(
|
|
__dirname,
|
|
'..',
|
|
'..',
|
|
'templates',
|
|
'device-plugin',
|
|
);
|
|
const templateExt = '.template';
|
|
|
|
export default class Init extends Command {
|
|
public static description =
|
|
'initializes a Flipper desktop plugin template in the provided directory';
|
|
|
|
public static examples = [`$ flipper-pkg init path/to/plugin`];
|
|
|
|
public static args: args.IArg[] = [
|
|
{
|
|
name: 'directory',
|
|
required: false,
|
|
default: '.',
|
|
description:
|
|
'Path to the directory where the plugin package template should be initialized. Defaults to the current working directory.',
|
|
},
|
|
];
|
|
|
|
public async run() {
|
|
const {args} = this.parse(Init);
|
|
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',
|
|
name: 'id',
|
|
message:
|
|
'ID (must match native plugin ID, e.g. returned by getId() in Android plugin):',
|
|
},
|
|
];
|
|
const id: string = (await inquirer.prompt(idQuestion)).id;
|
|
const titleQuestion: inquirer.QuestionCollection = [
|
|
{
|
|
type: 'input',
|
|
name: 'title',
|
|
message: 'Title (will be shown in the Flipper main sidebar):',
|
|
default: id,
|
|
},
|
|
];
|
|
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);
|
|
|
|
if (fs.existsSync(outputDirectory)) {
|
|
console.error(`Directory '${outputDirectory}' already exists`);
|
|
process.exit(1);
|
|
}
|
|
console.log(
|
|
`⚙️ Initializing Flipper desktop template in ${outputDirectory}`,
|
|
);
|
|
await fs.ensureDir(outputDirectory);
|
|
await initTemplate(
|
|
id,
|
|
title,
|
|
pluginType,
|
|
supportedDevices,
|
|
outputDirectory,
|
|
);
|
|
|
|
console.log(`⚙️ Installing dependencies`);
|
|
spawnSync('yarn', ['install'], {cwd: outputDirectory, stdio: [0, 1, 2]});
|
|
|
|
console.log(
|
|
`✅ Plugin directory initialized. Package name: ${packageName}.`,
|
|
);
|
|
console.log(
|
|
` Run 'cd ${packageName} && yarn watch' to get started! You might need to restart Flipper before the new plugin is detected.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getPackageNameFromId(id: string): string {
|
|
return 'flipper-plugin-' + id.toLowerCase().replace(/[^a-zA-Z0-9\-_]+/g, '-');
|
|
}
|
|
|
|
export async function initTemplate(
|
|
id: string,
|
|
title: string,
|
|
pluginType: PluginType,
|
|
supportedDevices: string[] | undefined,
|
|
outputDirectory: string,
|
|
) {
|
|
const flipper_version = JSON.parse(
|
|
await fs.readFile(
|
|
path.resolve(__dirname, '..', '..', 'package.json'),
|
|
'utf-8',
|
|
),
|
|
).version;
|
|
const packageName = getPackageNameFromId(id);
|
|
const templateDir =
|
|
pluginType === 'device' ? devicePluginTemplateDir : pluginTemplateDir;
|
|
const templateItems = await recursiveReaddir(templateDir);
|
|
|
|
for (const item of templateItems) {
|
|
const lstat = await fs.lstat(item);
|
|
if (lstat.isFile()) {
|
|
const file = path.relative(templateDir, item);
|
|
const dir = path.dirname(file);
|
|
const newDir = path.join(outputDirectory, dir);
|
|
const newFile = file.endsWith(templateExt)
|
|
? path.join(
|
|
outputDirectory,
|
|
file.substring(0, file.length - templateExt.length),
|
|
)
|
|
: path.join(outputDirectory, file);
|
|
await fs.ensureDir(newDir);
|
|
const content = (await fs.readFile(item))
|
|
.toString()
|
|
.replace('{{id}}', id)
|
|
.replace('{{title}}', title)
|
|
.replace('{{flipper_version}}', flipper_version)
|
|
.replace(
|
|
'{{supported_devices}}',
|
|
JSON.stringify(
|
|
supportedDevices
|
|
? supportedDevices.map((d) => ({
|
|
os: d,
|
|
}))
|
|
: [],
|
|
),
|
|
)
|
|
.replace('{{package_name}}', packageName);
|
|
await fs.writeFile(newFile, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function verifyFlipperSearchPath(pluginDirectory: string) {
|
|
const flipperConfigPath = path.join(homedir(), '.flipper', 'config.json');
|
|
if (!fs.existsSync(flipperConfigPath)) {
|
|
console.warn(
|
|
`It seems Flipper is not installed on your machine; failed to find ${flipperConfigPath}. Head to 'fbflipper.com' to download flipper`,
|
|
);
|
|
} else {
|
|
const config = JSON.parse(fs.readFileSync(flipperConfigPath, 'utf8'));
|
|
const pluginPaths: string[] = config.pluginPaths ?? [];
|
|
const isInSearchPath = pluginPaths.some((p) => {
|
|
// Match: exact path and first level subdirectory
|
|
const relativePath = path.relative(
|
|
path.resolve(p.replace(/^~/, homedir())),
|
|
pluginDirectory,
|
|
);
|
|
return relativePath.split('/').length === 1;
|
|
});
|
|
if (!isInSearchPath) {
|
|
if (
|
|
(
|
|
await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'addToPath',
|
|
message: `You are about to create a plugin in a directory that isn't watched by Flipper. Should we add ${pluginDirectory} to the Flipper search path? (Ctrl^C to abort)`,
|
|
default: true,
|
|
},
|
|
])
|
|
).addToPath
|
|
) {
|
|
fs.writeFileSync(
|
|
flipperConfigPath,
|
|
JSON.stringify(
|
|
{
|
|
...config,
|
|
pluginPaths: [...pluginPaths, pluginDirectory],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
console.log(
|
|
`⚙️ Added '${pluginDirectory}' to the search paths in '${flipperConfigPath}'`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|