setup webserver for flipper-server

Summary:
This sets up the metro bundler for flipper-server, to be able to serve the front end.

Note that this is a setup that is only relevant for development purposes

Done in this diff:

* setup metro
* setup fast refresh
* setup nodemon to be able to refresh on server changes

Not done in this diff

* Setup FlipperServerImpl in the flipper-server
* Load flipper-ui-core in flipper-ui-browser
* Load plugins
* Support options, env vars etc etc
* Make flipper-server stand alone (it is largely self contained, but still requires some static resources like theming)

Reviewed By: passy, aigoncharov

Differential Revision: D32626137

fbshipit-source-id: 47f580356ddf0993392d3b583082b187661727e9
This commit is contained in:
Michel Weststrate
2021-12-08 04:25:28 -08:00
committed by Facebook GitHub Bot
parent 2b81be6c29
commit 0dfc73da93
10 changed files with 759 additions and 18 deletions

View File

@@ -7,6 +7,13 @@
* @format
*/
export function helloWorld() {
return true;
}
// TODO: currently flipper-server is only suitable for development,
// needs to be come independently runnable, prebundled, distributed, etc!
// in future require conditionally
import {startWebServerDev} from './startWebServerDev';
import chalk from 'chalk';
startWebServerDev().catch((e) => {
console.error(chalk.red('Server error: '), e);
process.exit(1);
});

View File

@@ -0,0 +1,165 @@
/**
* 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 {hostname} from 'os';
import chalk from 'chalk';
import express, {Express} from 'express';
import http from 'http';
import path from 'path';
import fs from 'fs-extra';
import socketio from 'socket.io';
import {getWatchFolders} from 'flipper-pkg-lib';
import Metro from 'metro';
import pFilter from 'p-filter';
const PORT = 52342;
const rootDir = path.resolve(__dirname, '..', '..');
const staticDir = path.join(rootDir, 'static');
const babelTransformationsDir = path.resolve(
rootDir,
'babel-transformer',
'src',
);
const uiSourceDirs = [
'flipper-ui-browser',
'flipper-ui-core',
'flipper-plugin',
'flipper-common',
];
// This file is heavily inspired by scripts/start-dev-server.ts!
export async function startWebServerDev() {
checkDevServer();
// await prepareDefaultPlugins(
// process.env.FLIPPER_RELEASE_CHANNEL === 'insiders',
// );
// await ensurePluginFoldersWatchable();
const {app, server} = await startAssetServer(PORT);
const socket = await addWebsocket(server);
await startMetroServer(app, server, socket);
// await compileMain();
// if (dotenv && dotenv.parsed) {
// console.log('✅ Loaded env vars from .env file: ', dotenv.parsed);
// }
// shutdownElectron = launchElectron(port);
console.log(
`Flipper DEV server started at http://localhost:${PORT}/index.web.dev.html`,
);
}
function looksLikeDevServer(): boolean {
const hn = hostname();
if (/^devvm.*\.facebook\.com$/.test(hn)) {
return true;
}
if (hn.endsWith('.od.fbinfra.net')) {
return true;
}
return false;
}
function checkDevServer() {
if (looksLikeDevServer()) {
console.log(
chalk.red(
`✖ It looks like you're trying to start Flipper on your OnDemand or DevServer, which is not supported. Please run this in a local checkout on your laptop or desktop instead.`,
),
);
}
}
function startAssetServer(
port: number,
): Promise<{app: Express; server: http.Server}> {
const app = express();
app.use((_req, res, next) => {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
next();
});
app.get('/', (_req, res) => {
fs.readFile(path.join(staticDir, 'index.web.dev.html'), (_err, content) => {
res.end(content);
});
});
app.use(express.static(staticDir));
const server = http.createServer(app);
return new Promise((resolve) => {
server.listen(port, 'localhost', () => resolve({app, server}));
});
}
async function addWebsocket(server: http.Server) {
const io = new socketio.Server(server); // 3.1.0 socket.io doesn't have type definitions
io.on('connection', (client) => {
console.log(chalk.green(`Client connected ${client.id}`));
});
// Refresh the app on changes.
// When Fast Refresh enabled, reloads are performed by HMRClient, so don't need to watch manually here.
// if (!process.env.FLIPPER_FAST_REFRESH) {
// await startWatchChanges(io);
// }
return io;
}
async function startMetroServer(
app: Express,
server: http.Server,
socket: socketio.Server,
) {
const watchFolders = await dedupeFolders(
(
await Promise.all(
uiSourceDirs.map((dir) => getWatchFolders(path.resolve(rootDir, dir))),
)
).flat(),
);
// console.log('Source dirs\n\t' + watchFolders.join('\n\t'));
const baseConfig = await Metro.loadConfig();
const config = Object.assign({}, baseConfig, {
projectRoot: rootDir,
watchFolders,
transformer: {
...baseConfig.transformer,
babelTransformerPath: path.join(babelTransformationsDir, 'transform-app'),
},
resolver: {
...baseConfig.resolver,
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
blacklistRE: /\.native\.js$/,
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs', 'cjs'],
},
watch: true,
});
const connectMiddleware = await Metro.createConnectMiddleware(config);
app.use(connectMiddleware.middleware);
connectMiddleware.attachHmrServer(server);
app.use(function (err: any, _req: any, _res: any, next: any) {
console.error(chalk.red('\n\nCompile error in client bundle\n'), err);
socket.local.emit('hasErrors', err.toString());
next();
});
}
async function dedupeFolders(paths: string[]): Promise<string[]> {
return pFilter(
paths.filter((value, index, self) => self.indexOf(value) === index),
(f) => fs.pathExists(f),
);
}