Files
flipper/desktop/pkg/src/commands/init.ts
Michel Weststrate e7f841b6d2 Move flipper plugin from flipper-lib types to flipper-common
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
2021-12-08 04:30:55 -08:00

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}'`,
);
}
}
}
}