You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
9.3 KiB

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
* @oncall react_native
*/
"use strict";
const makeBuildCommand = require("./commands/build");
const makeDependenciesCommand = require("./commands/dependencies");
const makeServeCommand = require("./commands/serve");
const MetroHmrServer = require("./HmrServer");
const IncrementalBundler = require("./IncrementalBundler");
const createWebsocketServer = require("./lib/createWebsocketServer");
const MetroServer = require("./Server");
const outputBundle = require("./shared/output/bundle");
const chalk = require("chalk");
const fs = require("fs");
const http = require("http");
const https = require("https");
const { getDefaultConfig, loadConfig, mergeConfig } = require("metro-config");
const { InspectorProxy } = require("metro-inspector-proxy");
const net = require("net");
const { parse } = require("url");
const ws = require("ws");
async function getConfig(config) {
const defaultConfig = await getDefaultConfig(config.projectRoot);
return mergeConfig(defaultConfig, config);
}
async function runMetro(config, options) {
const mergedConfig = await getConfig(config);
const {
reporter,
server: { port },
} = mergedConfig;
reporter.update({
hasReducedPerformance: options
? Boolean(options.hasReducedPerformance)
: false,
port,
type: "initialize_started",
});
const { waitForBundler = false, ...serverOptions } =
options !== null && options !== void 0 ? options : {};
const server = new MetroServer(mergedConfig, serverOptions);
const readyPromise = server
.ready()
.then(() => {
reporter.update({
type: "initialize_done",
port,
});
})
.catch((error) => {
reporter.update({
type: "initialize_failed",
port,
error,
});
});
if (waitForBundler) {
await readyPromise;
}
return server;
}
exports.runMetro = runMetro;
exports.loadConfig = loadConfig;
const createConnectMiddleware = async function (config, options) {
const metroServer = await runMetro(config, options);
let enhancedMiddleware = metroServer.processRequest;
// Enhance the resulting middleware using the config options
if (config.server.enhanceMiddleware) {
enhancedMiddleware = config.server.enhanceMiddleware(
enhancedMiddleware,
metroServer
);
}
return {
attachHmrServer(httpServer) {
const wss = createWebsocketServer({
websocketServer: new MetroHmrServer(
metroServer.getBundler(),
metroServer.getCreateModuleId(),
config
),
});
httpServer.on("upgrade", (request, socket, head) => {
const { pathname } = parse(request.url);
if (pathname === "/hot") {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});
},
metroServer,
middleware: enhancedMiddleware,
end() {
metroServer.end();
},
};
};
exports.createConnectMiddleware = createConnectMiddleware;
exports.runServer = async (
config,
{
hasReducedPerformance = false,
host,
onError,
onReady,
secureServerOptions,
secure,
//deprecated
secureCert,
// deprecated
secureKey,
// deprecated
waitForBundler = false,
websocketEndpoints = {},
watch,
}
) => {
await earlyPortCheck(host, config.server.port);
if (secure != null || secureCert != null || secureKey != null) {
// eslint-disable-next-line no-console
console.warn(
chalk.inverse.yellow.bold(" DEPRECATED "),
"The `secure`, `secureCert`, and `secureKey` options are now deprecated. " +
"Please use the `secureServerOptions` object instead to pass options to " +
"Metro's https development server."
);
}
// Lazy require
const connect = require("connect");
const serverApp = connect();
const { middleware, end, metroServer } = await createConnectMiddleware(
config,
{
hasReducedPerformance,
waitForBundler,
watch,
}
);
serverApp.use(middleware);
let inspectorProxy = null;
if (config.server.runInspectorProxy) {
inspectorProxy = new InspectorProxy(config.projectRoot);
}
let httpServer;
if (secure || secureServerOptions != null) {
let options = secureServerOptions;
if (typeof secureKey === "string" && typeof secureCert === "string") {
options = {
key: fs.readFileSync(secureKey),
cert: fs.readFileSync(secureCert),
...secureServerOptions,
};
}
httpServer = https.createServer(options, serverApp);
} else {
httpServer = http.createServer(serverApp);
}
return new Promise((resolve, reject) => {
httpServer.on("error", (error) => {
if (onError) {
onError(error);
}
reject(error);
end();
});
httpServer.listen(config.server.port, host, () => {
if (onReady) {
onReady(httpServer);
}
Object.assign(websocketEndpoints, {
...(inspectorProxy
? {
...inspectorProxy.createWebSocketListeners(httpServer),
}
: {}),
"/hot": createWebsocketServer({
websocketServer: new MetroHmrServer(
metroServer.getBundler(),
metroServer.getCreateModuleId(),
config
),
}),
});
httpServer.on("upgrade", (request, socket, head) => {
const { pathname } = parse(request.url);
if (pathname != null && websocketEndpoints[pathname]) {
websocketEndpoints[pathname].handleUpgrade(
request,
socket,
head,
(ws) => {
websocketEndpoints[pathname].emit("connection", ws, request);
}
);
} else {
socket.destroy();
}
});
if (inspectorProxy) {
// TODO(hypuk): Refactor inspectorProxy.processRequest into separate request handlers
// so that we could provide routes (/json/list and /json/version) here.
// Currently this causes Metro to give warning about T31407894.
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
serverApp.use(inspectorProxy.processRequest.bind(inspectorProxy));
}
resolve(httpServer);
});
// Disable any kind of automatic timeout behavior for incoming
// requests in case it takes the packager more than the default
// timeout of 120 seconds to respond to a request.
httpServer.timeout = 0;
httpServer.on("close", () => {
end();
});
});
};
exports.runBuild = async (
config,
{
dev = false,
entry,
onBegin,
onComplete,
onProgress,
minify = true,
output = outputBundle,
out,
platform = "web",
sourceMap = false,
sourceMapUrl,
}
) => {
const metroServer = await runMetro(config, {
watch: false,
});
try {
const requestOptions = {
dev,
entryFile: entry,
inlineSourceMap: sourceMap && !sourceMapUrl,
minify,
platform,
sourceMapUrl: sourceMap === false ? undefined : sourceMapUrl,
createModuleIdFactory: config.serializer.createModuleIdFactory,
onProgress,
};
if (onBegin) {
onBegin();
}
const metroBundle = await output.build(metroServer, requestOptions);
if (onComplete) {
onComplete();
}
if (out) {
const bundleOutput = out.replace(/(\.js)?$/, ".js");
const sourcemapOutput =
sourceMap === false ? undefined : out.replace(/(\.js)?$/, ".map");
const outputOptions = {
bundleOutput,
sourcemapOutput,
dev,
platform,
};
// eslint-disable-next-line no-console
await output.save(metroBundle, outputOptions, console.log);
}
return metroBundle;
} finally {
await metroServer.end();
}
};
exports.buildGraph = async function (
config,
{
customTransformOptions = Object.create(null),
dev = false,
entries,
minify = false,
onProgress,
platform = "web",
type = "module",
}
) {
const mergedConfig = await getConfig(config);
const bundler = new IncrementalBundler(mergedConfig);
try {
const { customResolverOptions, ...defaultTransformInputOptions } =
MetroServer.DEFAULT_GRAPH_OPTIONS;
return await bundler.buildGraphForEntries(
entries,
{
...defaultTransformInputOptions,
customTransformOptions,
dev,
minify,
platform,
type,
},
{
customResolverOptions,
}
);
} finally {
bundler.end();
}
};
exports.attachMetroCli = function (yargs, options = {}) {
const { build = {}, serve = {}, dependencies = {} } = options;
yargs.strict();
if (build) {
yargs.command(makeBuildCommand());
}
if (serve) {
yargs.command(makeServeCommand());
}
if (dependencies) {
yargs.command(makeDependenciesCommand());
}
return yargs;
};
async function earlyPortCheck(host, port) {
const server = net.createServer((c) => c.end());
try {
await new Promise((resolve, reject) => {
server.on("error", (err) => {
reject(err);
});
server.listen(port, host, undefined, () => resolve());
});
} finally {
await new Promise((resolve) => server.close(() => resolve()));
}
}