Dev mode: fixed loading plugins located outside of the Flipper source root folder
Summary: Dev mode: fixed loading of plugins located outside of the Flipper source root folder, e.g. in ~/flipper-plugins as suggested in tutorial docs. Reviewed By: passy Differential Revision: D21306639 fbshipit-source-id: bb9044b25324065f0c12169b95fbe663da8d4305
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b27f8ee236
commit
c21ccedf14
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -20,8 +20,9 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch Current Jest Suite",
|
"name": "Launch Current Jest Suite",
|
||||||
|
"cwd": "${workspaceFolder}/desktop",
|
||||||
"program": "${workspaceFolder}/desktop/node_modules/.bin/jest",
|
"program": "${workspaceFolder}/desktop/node_modules/.bin/jest",
|
||||||
"args": ["--runInBand", "${relativeFile}"]
|
"args": ["--runInBand", "${file}"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import {parse} from '@babel/parser';
|
import {parse} from '@babel/parser';
|
||||||
import {transformFromAstSync} from '@babel/core';
|
import {transformFromAstSync} from '@babel/core';
|
||||||
import {default as generate} from '@babel/generator';
|
import {default as generate} from '@babel/generator';
|
||||||
const flipperRequires = require('../flipper-requires');
|
const flipperRequires = require('../plugin-flipper-requires');
|
||||||
|
|
||||||
const babelOptions = {
|
const babelOptions = {
|
||||||
ast: true,
|
ast: true,
|
||||||
43
desktop/babel-transformer/src/app-flipper-requires.ts
Normal file
43
desktop/babel-transformer/src/app-flipper-requires.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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 {CallExpression, Identifier} from '@babel/types';
|
||||||
|
import {NodePath} from '@babel/traverse';
|
||||||
|
import {
|
||||||
|
tryReplaceFlipperRequire,
|
||||||
|
tryReplaceGlobalReactUsage,
|
||||||
|
} from './replace-flipper-requires';
|
||||||
|
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
|
const sourceRootDir = resolve(__dirname, '..', '..');
|
||||||
|
|
||||||
|
function isExcludedPath(path: string) {
|
||||||
|
// We shouldn't apply transformations for the plugins which are part of the repository
|
||||||
|
// as they are bundled with Flipper app and all use the single "react" dependency.
|
||||||
|
// But we should apply it for the plugins outside of Flipper folder, so they can be loaded
|
||||||
|
// in dev mode and use "react" from Flipper bundle.
|
||||||
|
return path.startsWith(sourceRootDir);
|
||||||
|
}
|
||||||
|
module.exports = () => ({
|
||||||
|
visitor: {
|
||||||
|
CallExpression(path: NodePath<CallExpression>, state: any) {
|
||||||
|
if (isExcludedPath(state.file.opts.filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tryReplaceFlipperRequire(path);
|
||||||
|
},
|
||||||
|
Identifier(path: NodePath<Identifier>, state: any) {
|
||||||
|
if (isExcludedPath(state.file.opts.filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tryReplaceGlobalReactUsage(path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,22 +7,20 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {CallExpression, Identifier, isStringLiteral} from '@babel/types';
|
||||||
CallExpression,
|
|
||||||
Identifier,
|
|
||||||
isStringLiteral,
|
|
||||||
identifier,
|
|
||||||
} from '@babel/types';
|
|
||||||
import {NodePath} from '@babel/traverse';
|
import {NodePath} from '@babel/traverse';
|
||||||
|
import {resolve, dirname} from 'path';
|
||||||
|
|
||||||
import {resolve, dirname, relative} from 'path';
|
import {
|
||||||
|
tryReplaceFlipperRequire,
|
||||||
|
tryReplaceGlobalReactUsage,
|
||||||
|
} from './replace-flipper-requires';
|
||||||
|
|
||||||
// do not apply this transform for these paths
|
// do not apply this transform for these paths
|
||||||
const EXCLUDE_PATHS = [
|
const EXCLUDE_PATHS = [
|
||||||
'/node_modules/react-devtools-core/',
|
'/node_modules/react-devtools-core/',
|
||||||
'relay-devtools/DevtoolsUI',
|
'relay-devtools/DevtoolsUI',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isExcludedPath(path: string) {
|
function isExcludedPath(path: string) {
|
||||||
for (const epath of EXCLUDE_PATHS) {
|
for (const epath of EXCLUDE_PATHS) {
|
||||||
if (path.indexOf(epath) > -1) {
|
if (path.indexOf(epath) > -1) {
|
||||||
@@ -31,36 +29,20 @@ function isExcludedPath(path: string) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function isReactImportIdentifier(path: NodePath<Identifier>) {
|
|
||||||
return (
|
|
||||||
path.parentPath.node.type === 'ImportNamespaceSpecifier' &&
|
|
||||||
path.parentPath.node.local.name === 'React'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
module.exports = () => ({
|
module.exports = () => ({
|
||||||
visitor: {
|
visitor: {
|
||||||
CallExpression(path: NodePath<CallExpression>, state: any) {
|
CallExpression(path: NodePath<CallExpression>, state: any) {
|
||||||
if (isExcludedPath(state.file.opts.filename)) {
|
if (isExcludedPath(state.file.opts.filename)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = path.node;
|
if (!tryReplaceFlipperRequire(path)) {
|
||||||
const args = node.arguments || [];
|
const node = path.node;
|
||||||
|
const args = node.arguments || [];
|
||||||
if (
|
if (
|
||||||
node.callee.type === 'Identifier' &&
|
node.callee.type === 'Identifier' &&
|
||||||
node.callee.name === 'require' &&
|
node.callee.name === 'require' &&
|
||||||
args.length === 1 &&
|
args.length === 1 &&
|
||||||
isStringLiteral(args[0])
|
isStringLiteral(args[0]) &&
|
||||||
) {
|
|
||||||
if (args[0].value === 'flipper') {
|
|
||||||
path.replaceWith(identifier('global.Flipper'));
|
|
||||||
} else if (args[0].value === 'react') {
|
|
||||||
path.replaceWith(identifier('global.React'));
|
|
||||||
} else if (args[0].value === 'react-dom') {
|
|
||||||
path.replaceWith(identifier('global.ReactDOM'));
|
|
||||||
} else if (args[0].value === 'adbkit') {
|
|
||||||
path.replaceWith(identifier('global.adbkit'));
|
|
||||||
} else if (
|
|
||||||
// require a file not a pacakge
|
// require a file not a pacakge
|
||||||
args[0].value.indexOf('/') > -1 &&
|
args[0].value.indexOf('/') > -1 &&
|
||||||
// in the plugin itself and not inside one of its dependencies
|
// in the plugin itself and not inside one of its dependencies
|
||||||
@@ -68,14 +50,14 @@ module.exports = () => ({
|
|||||||
// the resolved path for this file is outside the plugins root
|
// the resolved path for this file is outside the plugins root
|
||||||
!resolve(
|
!resolve(
|
||||||
state.file.opts.root,
|
state.file.opts.root,
|
||||||
relative(state.file.opts.cwd, dirname(state.file.opts.filename)),
|
dirname(state.file.opts.filename),
|
||||||
args[0].value,
|
args[0].value,
|
||||||
).startsWith(state.file.opts.root)
|
).startsWith(state.file.opts.root)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Plugins cannot require files from outside their folder. Attempted to require ${resolve(
|
`Plugins cannot require files from outside their folder. Attempted to require ${resolve(
|
||||||
state.file.opts.root,
|
state.file.opts.root,
|
||||||
relative(state.file.opts.cwd, dirname(state.file.opts.filename)),
|
dirname(state.file.opts.filename),
|
||||||
args[0].value,
|
args[0].value,
|
||||||
)} which isn't inside ${state.file.opts.root}`,
|
)} which isn't inside ${state.file.opts.root}`,
|
||||||
);
|
);
|
||||||
@@ -83,14 +65,10 @@ module.exports = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Identifier(path: NodePath<Identifier>, state: any) {
|
Identifier(path: NodePath<Identifier>, state: any) {
|
||||||
if (
|
if (isExcludedPath(state.file.opts.filename)) {
|
||||||
path.node.name === 'React' &&
|
return;
|
||||||
(path.parentPath.node as any).id !== path.node &&
|
|
||||||
!isReactImportIdentifier(path) &&
|
|
||||||
!isExcludedPath(state.file.opts.filename)
|
|
||||||
) {
|
|
||||||
path.replaceWith(identifier('global.React'));
|
|
||||||
}
|
}
|
||||||
|
tryReplaceGlobalReactUsage(path);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
60
desktop/babel-transformer/src/replace-flipper-requires.ts
Normal file
60
desktop/babel-transformer/src/replace-flipper-requires.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
CallExpression,
|
||||||
|
isStringLiteral,
|
||||||
|
identifier,
|
||||||
|
Identifier,
|
||||||
|
} from '@babel/types';
|
||||||
|
import {NodePath} from '@babel/traverse';
|
||||||
|
|
||||||
|
const requireReplacements: any = {
|
||||||
|
flipper: 'global.Flipper',
|
||||||
|
react: 'global.React',
|
||||||
|
'react-dom': 'global.ReactDOM',
|
||||||
|
adbkit: 'global.adbkit',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function tryReplaceFlipperRequire(path: NodePath<CallExpression>) {
|
||||||
|
const node = path.node;
|
||||||
|
const args = node.arguments || [];
|
||||||
|
if (
|
||||||
|
node.callee.type === 'Identifier' &&
|
||||||
|
node.callee.name === 'require' &&
|
||||||
|
args.length === 1 &&
|
||||||
|
isStringLiteral(args[0])
|
||||||
|
) {
|
||||||
|
const replacement = requireReplacements[args[0].value];
|
||||||
|
if (replacement) {
|
||||||
|
path.replaceWith(identifier(replacement));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryReplaceGlobalReactUsage(path: NodePath<Identifier>) {
|
||||||
|
if (
|
||||||
|
path.node.name === 'React' &&
|
||||||
|
(path.parentPath.node as any).id !== path.node &&
|
||||||
|
!isReactImportIdentifier(path)
|
||||||
|
) {
|
||||||
|
path.replaceWith(identifier('global.React'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReactImportIdentifier(path: NodePath<Identifier>) {
|
||||||
|
return (
|
||||||
|
path.parentPath.node.type === 'ImportNamespaceSpecifier' &&
|
||||||
|
path.parentPath.node.local.name === 'React'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,5 +35,6 @@ function transform({
|
|||||||
}
|
}
|
||||||
plugins.push(require('./electron-requires'));
|
plugins.push(require('./electron-requires'));
|
||||||
plugins.push(require('./import-react'));
|
plugins.push(require('./import-react'));
|
||||||
|
plugins.push(require('./app-flipper-requires'));
|
||||||
return doTransform({filename, options, src, presets, plugins});
|
return doTransform({filename, options, src, presets, plugins});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ export default function transform({
|
|||||||
plugins.push(require('./electron-stubs'));
|
plugins.push(require('./electron-stubs'));
|
||||||
}
|
}
|
||||||
plugins.push(require('./electron-requires'));
|
plugins.push(require('./electron-requires'));
|
||||||
plugins.push(require('./flipper-requires'));
|
plugins.push(require('./plugin-flipper-requires'));
|
||||||
return doTransform({filename, options, src, presets, plugins});
|
return doTransform({filename, options, src, presets, plugins});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {default as generate} from '@babel/generator';
|
|||||||
import {parse} from '@babel/parser';
|
import {parse} from '@babel/parser';
|
||||||
import {transformFromAstSync} from '@babel/core';
|
import {transformFromAstSync} from '@babel/core';
|
||||||
import {default as flipperEnv} from './flipper-env';
|
import {default as flipperEnv} from './flipper-env';
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
export default function transform({
|
export default function transform({
|
||||||
filename,
|
filename,
|
||||||
@@ -25,6 +26,7 @@ export default function transform({
|
|||||||
presets?: any[];
|
presets?: any[];
|
||||||
plugins?: any[];
|
plugins?: any[];
|
||||||
}) {
|
}) {
|
||||||
|
filename = resolve(options.projectRoot, filename);
|
||||||
presets = presets ?? [require('@babel/preset-react')];
|
presets = presets ?? [require('@babel/preset-react')];
|
||||||
plugins = plugins ?? [];
|
plugins = plugins ?? [];
|
||||||
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
|
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import getAppWatchFolders from './get-app-watch-folders';
|
|||||||
import getPlugins from '../static/getPlugins';
|
import getPlugins from '../static/getPlugins';
|
||||||
import getPluginFolders from '../static/getPluginFolders';
|
import getPluginFolders from '../static/getPluginFolders';
|
||||||
import startWatchPlugins from '../static/startWatchPlugins';
|
import startWatchPlugins from '../static/startWatchPlugins';
|
||||||
|
import ensurePluginFoldersWatchable from '../static/ensurePluginFoldersWatchable';
|
||||||
|
|
||||||
const ansiToHtmlConverter = new AnsiToHtmlConverter();
|
const ansiToHtmlConverter = new AnsiToHtmlConverter();
|
||||||
|
|
||||||
@@ -268,12 +269,13 @@ function outputScreen(socket?: socketIo.Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
await generatePluginEntryPoints();
|
||||||
|
await ensurePluginFoldersWatchable();
|
||||||
const port = await detect(DEFAULT_PORT);
|
const port = await detect(DEFAULT_PORT);
|
||||||
const {app, server} = await startAssetServer(port);
|
const {app, server} = await startAssetServer(port);
|
||||||
const socket = await addWebsocket(server);
|
const socket = await addWebsocket(server);
|
||||||
await startMetroServer(app, server);
|
await startMetroServer(app, server);
|
||||||
outputScreen(socket);
|
outputScreen(socket);
|
||||||
await compileMain();
|
await compileMain();
|
||||||
await generatePluginEntryPoints();
|
|
||||||
shutdownElectron = launchElectron(port);
|
shutdownElectron = launchElectron(port);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {homedir} from 'os';
|
|||||||
import {runBuild, PluginDetails} from 'flipper-pkg-lib';
|
import {runBuild, PluginDetails} from 'flipper-pkg-lib';
|
||||||
import getPlugins from './getPlugins';
|
import getPlugins from './getPlugins';
|
||||||
import startWatchPlugins from './startWatchPlugins';
|
import startWatchPlugins from './startWatchPlugins';
|
||||||
|
import ensurePluginFoldersWatchable from './ensurePluginFoldersWatchable';
|
||||||
|
|
||||||
const HOME_DIR = homedir();
|
const HOME_DIR = homedir();
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ export default async function (
|
|||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
await ensurePluginFoldersWatchable();
|
||||||
options = Object.assign({}, DEFAULT_COMPILE_OPTIONS, options);
|
options = Object.assign({}, DEFAULT_COMPILE_OPTIONS, options);
|
||||||
const defaultPlugins = (
|
const defaultPlugins = (
|
||||||
await fs.readJson(path.join(__dirname, 'defaultPlugins', 'index.json'))
|
await fs.readJson(path.join(__dirname, 'defaultPlugins', 'index.json'))
|
||||||
|
|||||||
38
desktop/static/ensurePluginFoldersWatchable.ts
Normal file
38
desktop/static/ensurePluginFoldersWatchable.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 getPluginFolders from './getPluginFolders';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
|
const watchmanconfigName = '.watchmanconfig';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default async function ensurePluginFoldersWatchable() {
|
||||||
|
const pluginFolders = await getPluginFolders();
|
||||||
|
for (const pluginFolder of pluginFolders) {
|
||||||
|
if (!(await hasParentWithWatchmanConfig(pluginFolder))) {
|
||||||
|
// If no watchman config found in the plugins folder or any its parent, we need to create it.
|
||||||
|
// Otherwise we won't be able to listen for plugin changes.
|
||||||
|
await fs.writeJson(path.join(pluginFolder, watchmanconfigName), {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasParentWithWatchmanConfig(dir: string): Promise<boolean> {
|
||||||
|
if (await fs.pathExists(path.join(dir, watchmanconfigName))) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent && parent != '' && parent !== dir) {
|
||||||
|
return await hasParentWithWatchmanConfig(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user